codeceptjs 4.0.0-beta.2 → 4.0.0-beta.21
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 +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +73 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +262 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +301 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +109 -50
- package/lib/container.js +765 -261
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- 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 +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- 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 +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +54 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- 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 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- 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 +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils/typescript.js +237 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +124 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -875
- package/typings/types.d.ts +547 -992
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- 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 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
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
|
|
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'
|
|
15
|
-
import isElementClickable from './scripts/isElementClickable'
|
|
16
|
-
|
|
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'
|
|
17
16
|
import {
|
|
18
17
|
xpathLocator,
|
|
19
18
|
ucfirst,
|
|
@@ -27,33 +26,35 @@ import {
|
|
|
27
26
|
isModifierKey,
|
|
28
27
|
requireWithFallback,
|
|
29
28
|
normalizeSpacesInString,
|
|
30
|
-
} from '../utils.js'
|
|
31
|
-
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
dontSeeElementError,
|
|
44
|
-
seeElementError,
|
|
45
|
-
dontSeeElementInDOMError,
|
|
46
|
-
seeElementInDOMError,
|
|
47
|
-
} from './errors/ElementAssertion';
|
|
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'
|
|
40
|
+
|
|
41
|
+
let puppeteer
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
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
|
+
}
|
|
55
|
+
let perfTiming
|
|
56
|
+
const popupStore = new Popup()
|
|
57
|
+
const consoleLogStore = new Console()
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* ## Configuration
|
|
@@ -74,7 +75,7 @@ const consoleLogStore = new Console();
|
|
|
74
75
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to false.
|
|
75
76
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to false.
|
|
76
77
|
* @prop {number} [waitForAction=100] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
77
|
-
* @prop {string} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/
|
|
78
|
+
* @prop {string|string[]} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.waitforoptions.md). Array values are accepted as well.
|
|
78
79
|
* @prop {number} [pressKeyDelay=10] - delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField
|
|
79
80
|
* @prop {number} [getPageTimeout=30000] - config option to set maximum navigation time in milliseconds. If the timeout is set to 0, then timeout will be disabled.
|
|
80
81
|
* @prop {number} [waitForTimeout=1000] - default wait* timeout in ms.
|
|
@@ -82,13 +83,13 @@ const consoleLogStore = new Console();
|
|
|
82
83
|
* @prop {string} [userAgent] - user-agent string.
|
|
83
84
|
* @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
|
|
84
85
|
* @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
|
|
85
|
-
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/
|
|
86
|
+
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.launchoptions.md).
|
|
86
87
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
87
88
|
*/
|
|
88
|
-
const config = {}
|
|
89
|
+
const config = {}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Uses [Google Chrome's Puppeteer](https://github.com/
|
|
92
|
+
* Uses [Google Chrome's Puppeteer](https://github.com/puppeteer/puppeteer) library to run tests inside headless Chrome.
|
|
92
93
|
* Browser control is executed via DevTools Protocol (instead of Selenium).
|
|
93
94
|
* This helper works with a browser out of the box with no additional tools required to install.
|
|
94
95
|
*
|
|
@@ -222,29 +223,29 @@ const config = {};
|
|
|
222
223
|
*/
|
|
223
224
|
class Puppeteer extends Helper {
|
|
224
225
|
constructor(config) {
|
|
225
|
-
super(config)
|
|
226
|
+
super(config)
|
|
226
227
|
|
|
227
|
-
puppeteer
|
|
228
|
+
// puppeteer will be loaded dynamically in _init method
|
|
228
229
|
// set defaults
|
|
229
|
-
this.isRemoteBrowser = false
|
|
230
|
-
this.isRunning = false
|
|
231
|
-
this.isAuthenticated = false
|
|
232
|
-
this.sessionPages = {}
|
|
233
|
-
this.activeSessionName = ''
|
|
230
|
+
this.isRemoteBrowser = false
|
|
231
|
+
this.isRunning = false
|
|
232
|
+
this.isAuthenticated = false
|
|
233
|
+
this.sessionPages = {}
|
|
234
|
+
this.activeSessionName = ''
|
|
234
235
|
|
|
235
236
|
// for network stuff
|
|
236
|
-
this.requests = []
|
|
237
|
-
this.recording = false
|
|
238
|
-
this.recordedAtLeastOnce = false
|
|
237
|
+
this.requests = []
|
|
238
|
+
this.recording = false
|
|
239
|
+
this.recordedAtLeastOnce = false
|
|
239
240
|
|
|
240
241
|
// for websocket messages
|
|
241
|
-
this.webSocketMessages = []
|
|
242
|
-
this.recordingWebSocketMessages = false
|
|
243
|
-
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
244
|
-
this.cdpSession = null
|
|
242
|
+
this.webSocketMessages = []
|
|
243
|
+
this.recordingWebSocketMessages = false
|
|
244
|
+
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
245
|
+
this.cdpSession = null
|
|
245
246
|
|
|
246
247
|
// override defaults with config
|
|
247
|
-
this._setConfig(config)
|
|
248
|
+
this._setConfig(config)
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
_validateConfig(config) {
|
|
@@ -265,156 +266,201 @@ class Puppeteer extends Helper {
|
|
|
265
266
|
show: false,
|
|
266
267
|
defaultPopupAction: 'accept',
|
|
267
268
|
highlightElement: false,
|
|
268
|
-
}
|
|
269
|
+
}
|
|
269
270
|
|
|
270
|
-
return Object.assign(defaults, config)
|
|
271
|
+
return Object.assign(defaults, config)
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
_getOptions(config) {
|
|
274
|
-
return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome
|
|
275
|
+
return config.browser === 'firefox' ? Object.assign(this.options.firefox, { product: 'firefox' }) : this.options.chrome
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
_setConfig(config) {
|
|
278
|
-
this.options = this._validateConfig(config)
|
|
279
|
+
this.options = this._validateConfig(config)
|
|
279
280
|
this.puppeteerOptions = {
|
|
280
281
|
headless: !this.options.show,
|
|
281
282
|
...this._getOptions(config),
|
|
282
|
-
}
|
|
283
|
-
if (this.puppeteerOptions.headless) this.puppeteerOptions.headless = 'new'
|
|
284
|
-
this.isRemoteBrowser = !!this.puppeteerOptions.browserWSEndpoint
|
|
285
|
-
popupStore.defaultAction = this.options.defaultPopupAction
|
|
283
|
+
}
|
|
284
|
+
if (this.puppeteerOptions.headless) this.puppeteerOptions.headless = 'new'
|
|
285
|
+
this.isRemoteBrowser = !!this.puppeteerOptions.browserWSEndpoint
|
|
286
|
+
popupStore.defaultAction = this.options.defaultPopupAction
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
static _config() {
|
|
289
290
|
return [
|
|
290
291
|
{ name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
|
|
291
292
|
{
|
|
292
|
-
name: 'show',
|
|
293
|
+
name: 'show',
|
|
294
|
+
message: 'Show browser window',
|
|
295
|
+
default: true,
|
|
296
|
+
type: 'confirm',
|
|
293
297
|
},
|
|
294
298
|
{
|
|
295
|
-
name: 'windowSize',
|
|
299
|
+
name: 'windowSize',
|
|
300
|
+
message: 'Browser viewport size',
|
|
301
|
+
default: '1200x900',
|
|
296
302
|
},
|
|
297
|
-
]
|
|
303
|
+
]
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
static _checkRequirements() {
|
|
301
307
|
try {
|
|
302
|
-
|
|
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
|
|
303
311
|
} catch (e) {
|
|
304
|
-
return ['puppeteer']
|
|
312
|
+
return ['puppeteer']
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
_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
|
+
}
|
|
309
335
|
}
|
|
310
336
|
|
|
311
337
|
_beforeSuite() {
|
|
312
338
|
if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
|
|
313
|
-
this.debugSection('Session', 'Starting singleton browser session')
|
|
314
|
-
return this._startBrowser()
|
|
339
|
+
this.debugSection('Session', 'Starting singleton browser session')
|
|
340
|
+
return this._startBrowser()
|
|
315
341
|
}
|
|
316
342
|
}
|
|
317
343
|
|
|
318
344
|
async _before(test) {
|
|
319
|
-
this.sessionPages = {}
|
|
320
|
-
this.currentRunningTest = test
|
|
345
|
+
this.sessionPages = {}
|
|
346
|
+
this.currentRunningTest = test
|
|
321
347
|
recorder.retry({
|
|
322
|
-
retries:
|
|
348
|
+
retries: test?.opts?.conditionalRetries || 3,
|
|
323
349
|
when: err => {
|
|
324
|
-
if (!err || typeof
|
|
325
|
-
return false
|
|
350
|
+
if (!err || typeof err.message !== 'string') {
|
|
351
|
+
return false
|
|
326
352
|
}
|
|
327
353
|
// ignore context errors
|
|
328
|
-
return err.message.includes('context')
|
|
354
|
+
return err.message.includes('context')
|
|
329
355
|
},
|
|
330
|
-
})
|
|
331
|
-
if (this.options.restart && !this.options.manualStart) return this._startBrowser()
|
|
332
|
-
if (!this.isRunning && !this.options.manualStart) return this._startBrowser()
|
|
333
|
-
return this.browser
|
|
356
|
+
})
|
|
357
|
+
if (this.options.restart && !this.options.manualStart) return this._startBrowser()
|
|
358
|
+
if (!this.isRunning && !this.options.manualStart) return this._startBrowser()
|
|
359
|
+
return this.browser
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
async _after() {
|
|
337
|
-
if (!this.isRunning) return
|
|
363
|
+
if (!this.isRunning) return
|
|
364
|
+
|
|
365
|
+
// Clear popup state to prevent leakage between tests
|
|
366
|
+
popupStore.clear()
|
|
338
367
|
|
|
339
368
|
// close other sessions
|
|
340
|
-
const contexts = this.browser.browserContexts()
|
|
341
|
-
const defaultCtx = contexts.shift()
|
|
369
|
+
const contexts = this.browser.browserContexts()
|
|
370
|
+
const defaultCtx = contexts.shift()
|
|
342
371
|
|
|
343
|
-
await Promise.all(contexts.map(c => c.close()))
|
|
372
|
+
await Promise.all(contexts.map(c => c.close()))
|
|
344
373
|
|
|
345
374
|
if (this.options.restart) {
|
|
346
|
-
this.isRunning = false
|
|
347
|
-
return this._stopBrowser()
|
|
375
|
+
this.isRunning = false
|
|
376
|
+
return this._stopBrowser()
|
|
348
377
|
}
|
|
349
378
|
|
|
350
379
|
// ensure this.page is from default context
|
|
351
380
|
if (this.page) {
|
|
352
|
-
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
353
|
-
await this._setPage(await existingPages[0].page())
|
|
381
|
+
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
382
|
+
await this._setPage(await existingPages[0].page())
|
|
354
383
|
}
|
|
355
384
|
|
|
356
|
-
if (this.options.keepBrowserState) return
|
|
385
|
+
if (this.options.keepBrowserState) return
|
|
357
386
|
|
|
358
387
|
if (!this.options.keepCookies) {
|
|
359
|
-
this.debugSection('Session', 'cleaning cookies and localStorage')
|
|
360
|
-
await this.clearCookie()
|
|
388
|
+
this.debugSection('Session', 'cleaning cookies and localStorage')
|
|
389
|
+
await this.clearCookie()
|
|
361
390
|
}
|
|
362
|
-
const currentUrl = await this.grabCurrentUrl()
|
|
391
|
+
const currentUrl = await this.grabCurrentUrl()
|
|
363
392
|
|
|
364
393
|
if (currentUrl.startsWith('http')) {
|
|
365
|
-
await this.executeScript('localStorage.clear();').catch(
|
|
366
|
-
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
367
|
-
})
|
|
368
|
-
await this.executeScript('sessionStorage.clear();').catch(
|
|
369
|
-
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
370
|
-
})
|
|
394
|
+
await this.executeScript('localStorage.clear();').catch(err => {
|
|
395
|
+
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
396
|
+
})
|
|
397
|
+
await this.executeScript('sessionStorage.clear();').catch(err => {
|
|
398
|
+
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err
|
|
399
|
+
})
|
|
371
400
|
}
|
|
372
|
-
await this.closeOtherTabs()
|
|
373
|
-
return this.browser
|
|
401
|
+
await this.closeOtherTabs()
|
|
402
|
+
return this.browser
|
|
374
403
|
}
|
|
375
404
|
|
|
376
|
-
_afterSuite() {
|
|
377
|
-
}
|
|
405
|
+
_afterSuite() {}
|
|
378
406
|
|
|
379
407
|
_finishTest() {
|
|
380
|
-
if (!this.options.restart && this.isRunning) return this._stopBrowser()
|
|
408
|
+
if (!this.options.restart && this.isRunning) return this._stopBrowser()
|
|
381
409
|
}
|
|
382
410
|
|
|
383
411
|
_session() {
|
|
384
412
|
return {
|
|
385
413
|
start: async (name = '') => {
|
|
386
|
-
this.debugSection('Incognito Tab', 'opened')
|
|
387
|
-
this.activeSessionName = name
|
|
414
|
+
this.debugSection('Incognito Tab', 'opened')
|
|
415
|
+
this.activeSessionName = name
|
|
388
416
|
|
|
389
|
-
const bc = await this.browser.createBrowserContext()
|
|
390
|
-
await bc.newPage()
|
|
417
|
+
const bc = await this.browser.createBrowserContext()
|
|
418
|
+
await bc.newPage()
|
|
391
419
|
|
|
392
420
|
// Create a new page inside context.
|
|
393
|
-
return bc
|
|
421
|
+
return bc
|
|
394
422
|
},
|
|
395
423
|
stop: async () => {
|
|
396
424
|
// is closed by _after
|
|
397
425
|
},
|
|
398
|
-
loadVars: async
|
|
399
|
-
const existingPages = context.targets().filter(t => t.type() === 'page')
|
|
400
|
-
this.sessionPages[this.activeSessionName] = await existingPages[0].page()
|
|
401
|
-
return this._setPage(this.sessionPages[this.activeSessionName])
|
|
426
|
+
loadVars: async context => {
|
|
427
|
+
const existingPages = context.targets().filter(t => t.type() === 'page')
|
|
428
|
+
this.sessionPages[this.activeSessionName] = await existingPages[0].page()
|
|
429
|
+
return this._setPage(this.sessionPages[this.activeSessionName])
|
|
402
430
|
},
|
|
403
|
-
restoreVars: async
|
|
404
|
-
this.withinLocator = null
|
|
431
|
+
restoreVars: async session => {
|
|
432
|
+
this.withinLocator = null
|
|
405
433
|
|
|
406
434
|
if (!session) {
|
|
407
|
-
this.activeSessionName = ''
|
|
435
|
+
this.activeSessionName = ''
|
|
408
436
|
} else {
|
|
409
|
-
this.activeSessionName = session
|
|
437
|
+
this.activeSessionName = session
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const defaultCtx = this.browser.defaultBrowserContext()
|
|
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
|
|
410
459
|
}
|
|
411
|
-
const defaultCtx = this.browser.defaultBrowserContext();
|
|
412
|
-
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page');
|
|
413
|
-
await this._setPage(await existingPages[0].page());
|
|
414
460
|
|
|
415
|
-
return this._waitForAction()
|
|
461
|
+
return this._waitForAction()
|
|
416
462
|
},
|
|
417
|
-
}
|
|
463
|
+
}
|
|
418
464
|
}
|
|
419
465
|
|
|
420
466
|
/**
|
|
@@ -435,7 +481,7 @@ class Puppeteer extends Helper {
|
|
|
435
481
|
* @param {function} fn async function that is executed with Puppeteer as argument
|
|
436
482
|
*/
|
|
437
483
|
usePuppeteerTo(description, fn) {
|
|
438
|
-
return this._useTo(...arguments)
|
|
484
|
+
return this._useTo(...arguments)
|
|
439
485
|
}
|
|
440
486
|
|
|
441
487
|
/**
|
|
@@ -449,7 +495,7 @@ class Puppeteer extends Helper {
|
|
|
449
495
|
* ```
|
|
450
496
|
*/
|
|
451
497
|
amAcceptingPopups() {
|
|
452
|
-
popupStore.actionType = 'accept'
|
|
498
|
+
popupStore.actionType = 'accept'
|
|
453
499
|
}
|
|
454
500
|
|
|
455
501
|
/**
|
|
@@ -458,7 +504,7 @@ class Puppeteer extends Helper {
|
|
|
458
504
|
* libraries](http://jster.net/category/windows-modals-popups).
|
|
459
505
|
*/
|
|
460
506
|
acceptPopup() {
|
|
461
|
-
popupStore.assertPopupActionType('accept')
|
|
507
|
+
popupStore.assertPopupActionType('accept')
|
|
462
508
|
}
|
|
463
509
|
|
|
464
510
|
/**
|
|
@@ -472,23 +518,23 @@ class Puppeteer extends Helper {
|
|
|
472
518
|
* ```
|
|
473
519
|
*/
|
|
474
520
|
amCancellingPopups() {
|
|
475
|
-
popupStore.actionType = 'cancel'
|
|
521
|
+
popupStore.actionType = 'cancel'
|
|
476
522
|
}
|
|
477
523
|
|
|
478
524
|
/**
|
|
479
525
|
* Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
|
|
480
526
|
*/
|
|
481
527
|
cancelPopup() {
|
|
482
|
-
popupStore.assertPopupActionType('cancel')
|
|
528
|
+
popupStore.assertPopupActionType('cancel')
|
|
483
529
|
}
|
|
484
530
|
|
|
485
531
|
/**
|
|
486
532
|
* {{> seeInPopup }}
|
|
487
533
|
*/
|
|
488
534
|
async seeInPopup(text) {
|
|
489
|
-
popupStore.assertPopupVisible()
|
|
490
|
-
const popupText = await popupStore.popup.message()
|
|
491
|
-
stringIncludes('text in popup').assert(text, popupText)
|
|
535
|
+
popupStore.assertPopupVisible()
|
|
536
|
+
const popupText = await popupStore.popup.message()
|
|
537
|
+
stringIncludes('text in popup').assert(text, popupText)
|
|
492
538
|
}
|
|
493
539
|
|
|
494
540
|
/**
|
|
@@ -496,25 +542,25 @@ class Puppeteer extends Helper {
|
|
|
496
542
|
* @param {object} page page to set
|
|
497
543
|
*/
|
|
498
544
|
async _setPage(page) {
|
|
499
|
-
page = await page
|
|
500
|
-
this._addPopupListener(page)
|
|
501
|
-
this._addErrorListener(page)
|
|
502
|
-
this.page = page
|
|
503
|
-
if (!page) return
|
|
504
|
-
page.setDefaultNavigationTimeout(this.options.getPageTimeout)
|
|
505
|
-
this.context = await this.page.$('body')
|
|
545
|
+
page = await page
|
|
546
|
+
this._addPopupListener(page)
|
|
547
|
+
this._addErrorListener(page)
|
|
548
|
+
this.page = page
|
|
549
|
+
if (!page) return
|
|
550
|
+
page.setDefaultNavigationTimeout(this.options.getPageTimeout)
|
|
551
|
+
this.context = await this.page.$('body')
|
|
506
552
|
if (this.options.browser === 'chrome') {
|
|
507
|
-
await page.bringToFront()
|
|
553
|
+
await page.bringToFront()
|
|
508
554
|
}
|
|
509
555
|
}
|
|
510
556
|
|
|
511
557
|
async _addErrorListener(page) {
|
|
512
558
|
if (!page) {
|
|
513
|
-
return
|
|
559
|
+
return
|
|
514
560
|
}
|
|
515
|
-
page.on('error', async
|
|
516
|
-
console.error('Puppeteer page error', error)
|
|
517
|
-
})
|
|
561
|
+
page.on('error', async error => {
|
|
562
|
+
console.error('Puppeteer page error', error)
|
|
563
|
+
})
|
|
518
564
|
}
|
|
519
565
|
|
|
520
566
|
/**
|
|
@@ -526,32 +572,32 @@ class Puppeteer extends Helper {
|
|
|
526
572
|
*/
|
|
527
573
|
_addPopupListener(page) {
|
|
528
574
|
if (!page) {
|
|
529
|
-
return
|
|
575
|
+
return
|
|
530
576
|
}
|
|
531
|
-
page.on('dialog', async
|
|
532
|
-
popupStore.popup = dialog
|
|
533
|
-
const action = popupStore.actionType || this.options.defaultPopupAction
|
|
534
|
-
await this._waitForAction()
|
|
577
|
+
page.on('dialog', async dialog => {
|
|
578
|
+
popupStore.popup = dialog
|
|
579
|
+
const action = popupStore.actionType || this.options.defaultPopupAction
|
|
580
|
+
await this._waitForAction()
|
|
535
581
|
|
|
536
582
|
switch (action) {
|
|
537
583
|
case 'accept':
|
|
538
|
-
return dialog.accept()
|
|
584
|
+
return dialog.accept()
|
|
539
585
|
|
|
540
586
|
case 'cancel':
|
|
541
|
-
return dialog.dismiss()
|
|
587
|
+
return dialog.dismiss()
|
|
542
588
|
|
|
543
589
|
default: {
|
|
544
|
-
throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted')
|
|
590
|
+
throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted')
|
|
545
591
|
}
|
|
546
592
|
}
|
|
547
|
-
})
|
|
593
|
+
})
|
|
548
594
|
}
|
|
549
595
|
|
|
550
596
|
/**
|
|
551
597
|
* Gets page URL including hash.
|
|
552
598
|
*/
|
|
553
599
|
async _getPageUrl() {
|
|
554
|
-
return this.executeScript(() => window.location.href)
|
|
600
|
+
return this.executeScript(() => window.location.href)
|
|
555
601
|
}
|
|
556
602
|
|
|
557
603
|
/**
|
|
@@ -564,138 +610,167 @@ class Puppeteer extends Helper {
|
|
|
564
610
|
*/
|
|
565
611
|
async grabPopupText() {
|
|
566
612
|
if (popupStore.popup) {
|
|
567
|
-
return popupStore.popup.message()
|
|
613
|
+
return popupStore.popup.message()
|
|
568
614
|
}
|
|
569
|
-
return null
|
|
615
|
+
return null
|
|
570
616
|
}
|
|
571
617
|
|
|
572
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
|
+
|
|
573
625
|
if (this.isRemoteBrowser) {
|
|
574
626
|
try {
|
|
575
|
-
this.browser = await puppeteer.connect(this.puppeteerOptions)
|
|
627
|
+
this.browser = await puppeteer.connect(this.puppeteerOptions)
|
|
576
628
|
} catch (err) {
|
|
577
629
|
if (err.toString().indexOf('ECONNREFUSED')) {
|
|
578
|
-
throw new RemoteBrowserConnectionRefused(err)
|
|
630
|
+
throw new RemoteBrowserConnectionRefused(err)
|
|
579
631
|
}
|
|
580
|
-
throw err
|
|
632
|
+
throw err
|
|
581
633
|
}
|
|
582
634
|
} else {
|
|
583
|
-
this.browser = await puppeteer.launch(this.puppeteerOptions)
|
|
635
|
+
this.browser = await puppeteer.launch(this.puppeteerOptions)
|
|
584
636
|
}
|
|
585
637
|
|
|
586
|
-
this.browser.on('targetcreated', target =>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
638
|
+
this.browser.on('targetcreated', target =>
|
|
639
|
+
target
|
|
640
|
+
.page()
|
|
641
|
+
.then(page => targetCreatedHandler.call(this, page))
|
|
642
|
+
.catch(e => {
|
|
643
|
+
console.error('Puppeteer page error', e)
|
|
644
|
+
}),
|
|
645
|
+
)
|
|
646
|
+
this.browser.on('targetchanged', target => {
|
|
647
|
+
this.debugSection('Url', target.url())
|
|
648
|
+
})
|
|
592
649
|
|
|
593
|
-
const existingPages = await this.browser.pages()
|
|
594
|
-
const mainPage = existingPages[0] || (await this.browser.newPage())
|
|
650
|
+
const existingPages = await this.browser.pages()
|
|
651
|
+
const mainPage = existingPages[0] || (await this.browser.newPage())
|
|
595
652
|
|
|
596
653
|
if (existingPages.length) {
|
|
597
654
|
// Run the handler as it will not be triggered if the page already exists
|
|
598
|
-
targetCreatedHandler.call(this, mainPage)
|
|
655
|
+
targetCreatedHandler.call(this, mainPage)
|
|
599
656
|
}
|
|
600
|
-
await this._setPage(mainPage)
|
|
601
|
-
await this.closeOtherTabs()
|
|
657
|
+
await this._setPage(mainPage)
|
|
658
|
+
await this.closeOtherTabs()
|
|
602
659
|
|
|
603
|
-
this.isRunning = true
|
|
660
|
+
this.isRunning = true
|
|
604
661
|
}
|
|
605
662
|
|
|
606
663
|
async _stopBrowser() {
|
|
607
|
-
this.withinLocator = null
|
|
608
|
-
this._setPage(null)
|
|
609
|
-
this.context = null
|
|
610
|
-
popupStore.clear()
|
|
611
|
-
this.isAuthenticated = false
|
|
612
|
-
await this.browser.close()
|
|
664
|
+
this.withinLocator = null
|
|
665
|
+
this._setPage(null)
|
|
666
|
+
this.context = null
|
|
667
|
+
popupStore.clear()
|
|
668
|
+
this.isAuthenticated = false
|
|
669
|
+
await this.browser.close()
|
|
613
670
|
if (this.isRemoteBrowser) {
|
|
614
|
-
await this.browser.disconnect()
|
|
671
|
+
await this.browser.disconnect()
|
|
615
672
|
}
|
|
616
673
|
}
|
|
617
674
|
|
|
618
|
-
async _evaluateHandeInContext(...args) {
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
681
|
+
const context = await this._getContext()
|
|
682
|
+
return context.evaluateHandle(fn, ...args)
|
|
621
683
|
}
|
|
622
684
|
|
|
623
685
|
async _withinBegin(locator) {
|
|
624
686
|
if (this.withinLocator) {
|
|
625
|
-
throw new Error(
|
|
687
|
+
throw new Error("Can't start within block inside another within block")
|
|
626
688
|
}
|
|
627
689
|
|
|
628
|
-
const frame = isFrameLocator(locator)
|
|
690
|
+
const frame = isFrameLocator(locator)
|
|
629
691
|
|
|
630
692
|
if (frame) {
|
|
631
693
|
if (Array.isArray(frame)) {
|
|
632
|
-
return this.switchTo(null)
|
|
633
|
-
.then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()));
|
|
694
|
+
return this.switchTo(null).then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()))
|
|
634
695
|
}
|
|
635
|
-
await this.switchTo(frame)
|
|
636
|
-
this.withinLocator = new Locator(frame)
|
|
637
|
-
return
|
|
696
|
+
await this.switchTo(frame)
|
|
697
|
+
this.withinLocator = new Locator(frame)
|
|
698
|
+
return
|
|
638
699
|
}
|
|
639
700
|
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
701
|
+
const el = await this._locateElement(locator)
|
|
702
|
+
if (!el) {
|
|
703
|
+
throw new ElementNotFound(locator, 'Element for within context')
|
|
704
|
+
}
|
|
705
|
+
this.context = el
|
|
643
706
|
|
|
644
|
-
this.withinLocator = new Locator(locator)
|
|
707
|
+
this.withinLocator = new Locator(locator)
|
|
645
708
|
}
|
|
646
709
|
|
|
647
710
|
async _withinEnd() {
|
|
648
|
-
this.withinLocator = null
|
|
649
|
-
this.
|
|
711
|
+
this.withinLocator = null
|
|
712
|
+
if (this.page && !this.page.isClosed?.()) {
|
|
713
|
+
this.context = await this.page.mainFrame().$('body')
|
|
714
|
+
} else {
|
|
715
|
+
this.context = null
|
|
716
|
+
}
|
|
650
717
|
}
|
|
651
718
|
|
|
652
719
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
653
|
-
const navigationStart = timing.navigationStart
|
|
720
|
+
const navigationStart = timing.navigationStart
|
|
654
721
|
|
|
655
|
-
const extractedData = {}
|
|
656
|
-
dataNames.forEach(
|
|
657
|
-
extractedData[name] = timing[name] - navigationStart
|
|
658
|
-
})
|
|
722
|
+
const extractedData = {}
|
|
723
|
+
dataNames.forEach(name => {
|
|
724
|
+
extractedData[name] = timing[name] - navigationStart
|
|
725
|
+
})
|
|
659
726
|
|
|
660
|
-
return extractedData
|
|
727
|
+
return extractedData
|
|
661
728
|
}
|
|
662
729
|
|
|
663
730
|
/**
|
|
664
731
|
* {{> amOnPage }}
|
|
665
732
|
*/
|
|
666
733
|
async amOnPage(url) {
|
|
667
|
-
if (
|
|
668
|
-
url = this.options.url + url
|
|
734
|
+
if (!/^\w+\:\/\//.test(url)) {
|
|
735
|
+
url = this.options.url + url
|
|
669
736
|
}
|
|
670
737
|
|
|
671
|
-
if (this.options.basicAuth &&
|
|
738
|
+
if (this.options.basicAuth && this.isAuthenticated !== true) {
|
|
672
739
|
if (url.includes(this.options.url)) {
|
|
673
|
-
await this.page.authenticate(this.options.basicAuth)
|
|
674
|
-
this.isAuthenticated = true
|
|
740
|
+
await this.page.authenticate(this.options.basicAuth)
|
|
741
|
+
this.isAuthenticated = true
|
|
675
742
|
}
|
|
676
743
|
}
|
|
677
744
|
|
|
678
745
|
if (this.options.trace) {
|
|
679
|
-
const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json
|
|
680
|
-
const dir = path.dirname(fileName)
|
|
681
|
-
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
682
|
-
await this.page.tracing.start({ screenshots: true, path: fileName })
|
|
683
|
-
this.currentRunningTest.artifacts.trace = fileName
|
|
746
|
+
const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
|
|
747
|
+
const dir = path.dirname(fileName)
|
|
748
|
+
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
749
|
+
await this.page.tracing.start({ screenshots: true, path: fileName })
|
|
750
|
+
this.currentRunningTest.artifacts.trace = fileName
|
|
684
751
|
}
|
|
685
752
|
|
|
686
|
-
|
|
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
|
+
}
|
|
687
768
|
|
|
688
|
-
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
769
|
+
const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing)))
|
|
689
770
|
|
|
690
|
-
perfTiming = this._extractDataFromPerformanceTiming(
|
|
691
|
-
performanceTiming,
|
|
692
|
-
'responseEnd',
|
|
693
|
-
'domInteractive',
|
|
694
|
-
'domContentLoadedEventEnd',
|
|
695
|
-
'loadEventEnd',
|
|
696
|
-
);
|
|
771
|
+
perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd')
|
|
697
772
|
|
|
698
|
-
return this._waitForAction()
|
|
773
|
+
return this._waitForAction()
|
|
699
774
|
}
|
|
700
775
|
|
|
701
776
|
/**
|
|
@@ -709,11 +784,11 @@ class Puppeteer extends Helper {
|
|
|
709
784
|
*/
|
|
710
785
|
async resizeWindow(width, height) {
|
|
711
786
|
if (width === 'maximize') {
|
|
712
|
-
throw new Error(
|
|
787
|
+
throw new Error("Puppeteer can't control windows, so it can't maximize it")
|
|
713
788
|
}
|
|
714
789
|
|
|
715
|
-
await this.page.setViewport({ width, height })
|
|
716
|
-
return this._waitForAction()
|
|
790
|
+
await this.page.setViewport({ width, height })
|
|
791
|
+
return this._waitForAction()
|
|
717
792
|
}
|
|
718
793
|
|
|
719
794
|
/**
|
|
@@ -729,9 +804,9 @@ class Puppeteer extends Helper {
|
|
|
729
804
|
*/
|
|
730
805
|
async setPuppeteerRequestHeaders(customHeaders) {
|
|
731
806
|
if (!customHeaders) {
|
|
732
|
-
throw new Error('Cannot send empty headers.')
|
|
807
|
+
throw new Error('Cannot send empty headers.')
|
|
733
808
|
}
|
|
734
|
-
return this.page.setExtraHTTPHeaders(customHeaders)
|
|
809
|
+
return this.page.setExtraHTTPHeaders(customHeaders)
|
|
735
810
|
}
|
|
736
811
|
|
|
737
812
|
/**
|
|
@@ -739,13 +814,15 @@ class Puppeteer extends Helper {
|
|
|
739
814
|
* {{ react }}
|
|
740
815
|
*/
|
|
741
816
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
742
|
-
const
|
|
743
|
-
|
|
817
|
+
const el = await this._locateElement(locator)
|
|
818
|
+
if (!el) {
|
|
819
|
+
throw new ElementNotFound(locator, 'Element to move cursor to')
|
|
820
|
+
}
|
|
744
821
|
|
|
745
822
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
746
|
-
const { x, y } = await getClickablePoint(
|
|
747
|
-
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
748
|
-
return this._waitForAction()
|
|
823
|
+
const { x, y } = await getClickablePoint(el)
|
|
824
|
+
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
825
|
+
return this._waitForAction()
|
|
749
826
|
}
|
|
750
827
|
|
|
751
828
|
/**
|
|
@@ -753,13 +830,14 @@ class Puppeteer extends Helper {
|
|
|
753
830
|
*
|
|
754
831
|
*/
|
|
755
832
|
async focus(locator) {
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
833
|
+
const el = await this._locateElement(locator)
|
|
834
|
+
if (!el) {
|
|
835
|
+
throw new ElementNotFound(locator, 'Element to focus')
|
|
836
|
+
}
|
|
759
837
|
|
|
760
|
-
await el.click()
|
|
761
|
-
await el.focus()
|
|
762
|
-
return this._waitForAction()
|
|
838
|
+
await el.click()
|
|
839
|
+
await el.focus()
|
|
840
|
+
return this._waitForAction()
|
|
763
841
|
}
|
|
764
842
|
|
|
765
843
|
/**
|
|
@@ -767,25 +845,27 @@ class Puppeteer extends Helper {
|
|
|
767
845
|
*
|
|
768
846
|
*/
|
|
769
847
|
async blur(locator) {
|
|
770
|
-
const
|
|
771
|
-
|
|
848
|
+
const el = await this._locateElement(locator)
|
|
849
|
+
if (!el) {
|
|
850
|
+
throw new ElementNotFound(locator, 'Element to blur')
|
|
851
|
+
}
|
|
772
852
|
|
|
773
|
-
await blurElement(
|
|
774
|
-
return this._waitForAction()
|
|
853
|
+
await blurElement(el, this.page)
|
|
854
|
+
return this._waitForAction()
|
|
775
855
|
}
|
|
776
856
|
|
|
777
857
|
/**
|
|
778
858
|
* {{> dragAndDrop }}
|
|
779
859
|
*/
|
|
780
860
|
async dragAndDrop(srcElement, destElement) {
|
|
781
|
-
return proceedDragAndDrop.call(this, srcElement, destElement)
|
|
861
|
+
return proceedDragAndDrop.call(this, srcElement, destElement)
|
|
782
862
|
}
|
|
783
863
|
|
|
784
864
|
/**
|
|
785
865
|
* {{> refreshPage }}
|
|
786
866
|
*/
|
|
787
867
|
async refreshPage() {
|
|
788
|
-
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation })
|
|
868
|
+
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation })
|
|
789
869
|
}
|
|
790
870
|
|
|
791
871
|
/**
|
|
@@ -793,8 +873,8 @@ class Puppeteer extends Helper {
|
|
|
793
873
|
*/
|
|
794
874
|
scrollPageToTop() {
|
|
795
875
|
return this.executeScript(() => {
|
|
796
|
-
window.scrollTo(0, 0)
|
|
797
|
-
})
|
|
876
|
+
window.scrollTo(0, 0)
|
|
877
|
+
})
|
|
798
878
|
}
|
|
799
879
|
|
|
800
880
|
/**
|
|
@@ -802,16 +882,10 @@ class Puppeteer extends Helper {
|
|
|
802
882
|
*/
|
|
803
883
|
scrollPageToBottom() {
|
|
804
884
|
return this.executeScript(() => {
|
|
805
|
-
const body = document.body
|
|
806
|
-
const html = document.documentElement
|
|
807
|
-
window.scrollTo(0, Math.max(
|
|
808
|
-
|
|
809
|
-
body.offsetHeight,
|
|
810
|
-
html.clientHeight,
|
|
811
|
-
html.scrollHeight,
|
|
812
|
-
html.offsetHeight,
|
|
813
|
-
));
|
|
814
|
-
});
|
|
885
|
+
const body = document.body
|
|
886
|
+
const html = document.documentElement
|
|
887
|
+
window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
|
|
888
|
+
})
|
|
815
889
|
}
|
|
816
890
|
|
|
817
891
|
/**
|
|
@@ -819,68 +893,68 @@ class Puppeteer extends Helper {
|
|
|
819
893
|
*/
|
|
820
894
|
async scrollTo(locator, offsetX = 0, offsetY = 0) {
|
|
821
895
|
if (typeof locator === 'number' && typeof offsetX === 'number') {
|
|
822
|
-
offsetY = offsetX
|
|
823
|
-
offsetX = locator
|
|
824
|
-
locator = null
|
|
896
|
+
offsetY = offsetX
|
|
897
|
+
offsetX = locator
|
|
898
|
+
locator = null
|
|
825
899
|
}
|
|
826
900
|
|
|
827
901
|
if (locator) {
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
902
|
+
const el = await this._locateElement(locator)
|
|
903
|
+
if (!el) {
|
|
904
|
+
throw new ElementNotFound(locator, 'Element to scroll into view')
|
|
905
|
+
}
|
|
906
|
+
await el.evaluate(el => el.scrollIntoView())
|
|
907
|
+
const elementCoordinates = await getClickablePoint(el)
|
|
908
|
+
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
|
|
834
909
|
} else {
|
|
835
|
-
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
|
|
910
|
+
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
|
|
836
911
|
}
|
|
837
|
-
return this._waitForAction()
|
|
912
|
+
return this._waitForAction()
|
|
838
913
|
}
|
|
839
914
|
|
|
840
915
|
/**
|
|
841
916
|
* {{> seeInTitle }}
|
|
842
917
|
*/
|
|
843
918
|
async seeInTitle(text) {
|
|
844
|
-
const title = await this.page.title()
|
|
845
|
-
stringIncludes('web page title').assert(text, title)
|
|
919
|
+
const title = await this.page.title()
|
|
920
|
+
stringIncludes('web page title').assert(text, title)
|
|
846
921
|
}
|
|
847
922
|
|
|
848
923
|
/**
|
|
849
924
|
* {{> grabPageScrollPosition }}
|
|
850
925
|
*/
|
|
851
926
|
async grabPageScrollPosition() {
|
|
852
|
-
/* eslint-disable comma-dangle */
|
|
853
927
|
function getScrollPosition() {
|
|
854
928
|
return {
|
|
855
929
|
x: window.pageXOffset,
|
|
856
|
-
y: window.pageYOffset
|
|
857
|
-
}
|
|
930
|
+
y: window.pageYOffset,
|
|
931
|
+
}
|
|
858
932
|
}
|
|
859
|
-
|
|
860
|
-
return this.executeScript(getScrollPosition)
|
|
933
|
+
|
|
934
|
+
return this.executeScript(getScrollPosition)
|
|
861
935
|
}
|
|
862
936
|
|
|
863
937
|
/**
|
|
864
938
|
* {{> seeTitleEquals }}
|
|
865
939
|
*/
|
|
866
940
|
async seeTitleEquals(text) {
|
|
867
|
-
const title = await this.page.title()
|
|
868
|
-
return equals('web page title').assert(title, text)
|
|
941
|
+
const title = await this.page.title()
|
|
942
|
+
return equals('web page title').assert(title, text)
|
|
869
943
|
}
|
|
870
944
|
|
|
871
945
|
/**
|
|
872
946
|
* {{> dontSeeInTitle }}
|
|
873
947
|
*/
|
|
874
948
|
async dontSeeInTitle(text) {
|
|
875
|
-
const title = await this.page.title()
|
|
876
|
-
stringIncludes('web page title').negate(text, title)
|
|
949
|
+
const title = await this.page.title()
|
|
950
|
+
stringIncludes('web page title').negate(text, title)
|
|
877
951
|
}
|
|
878
952
|
|
|
879
953
|
/**
|
|
880
954
|
* {{> grabTitle }}
|
|
881
955
|
*/
|
|
882
956
|
async grabTitle() {
|
|
883
|
-
return this.page.title()
|
|
957
|
+
return this.page.title()
|
|
884
958
|
}
|
|
885
959
|
|
|
886
960
|
/**
|
|
@@ -894,8 +968,23 @@ class Puppeteer extends Helper {
|
|
|
894
968
|
* {{ react }}
|
|
895
969
|
*/
|
|
896
970
|
async _locate(locator) {
|
|
897
|
-
const context = await this.context
|
|
898
|
-
return findElements.call(this, context, locator)
|
|
971
|
+
const context = await this.context
|
|
972
|
+
return findElements.call(this, context, locator)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Get single element by different locator types, including strict locator
|
|
977
|
+
* Should be used in custom helpers:
|
|
978
|
+
*
|
|
979
|
+
* ```js
|
|
980
|
+
* const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
|
|
981
|
+
* ```
|
|
982
|
+
*
|
|
983
|
+
* {{ react }}
|
|
984
|
+
*/
|
|
985
|
+
async _locateElement(locator) {
|
|
986
|
+
const context = await this.context
|
|
987
|
+
return findElement.call(this, context, locator)
|
|
899
988
|
}
|
|
900
989
|
|
|
901
990
|
/**
|
|
@@ -907,10 +996,12 @@ class Puppeteer extends Helper {
|
|
|
907
996
|
* ```
|
|
908
997
|
*/
|
|
909
998
|
async _locateCheckable(locator, providedContext = null) {
|
|
910
|
-
const context = providedContext || (await this._getContext())
|
|
911
|
-
const els = await findCheckable.call(this, locator, context)
|
|
912
|
-
|
|
913
|
-
|
|
999
|
+
const context = providedContext || (await this._getContext())
|
|
1000
|
+
const els = await findCheckable.call(this, locator, context)
|
|
1001
|
+
if (!els || els.length === 0) {
|
|
1002
|
+
throw new ElementNotFound(locator, 'Checkbox or radio')
|
|
1003
|
+
}
|
|
1004
|
+
return els[0]
|
|
914
1005
|
}
|
|
915
1006
|
|
|
916
1007
|
/**
|
|
@@ -921,8 +1012,8 @@ class Puppeteer extends Helper {
|
|
|
921
1012
|
* ```
|
|
922
1013
|
*/
|
|
923
1014
|
async _locateClickable(locator) {
|
|
924
|
-
const context = await this.context
|
|
925
|
-
return findClickable.call(this, context, locator)
|
|
1015
|
+
const context = await this.context
|
|
1016
|
+
return findClickable.call(this, context, locator)
|
|
926
1017
|
}
|
|
927
1018
|
|
|
928
1019
|
/**
|
|
@@ -933,7 +1024,7 @@ class Puppeteer extends Helper {
|
|
|
933
1024
|
* ```
|
|
934
1025
|
*/
|
|
935
1026
|
async _locateFields(locator) {
|
|
936
|
-
return findFields.call(this, locator)
|
|
1027
|
+
return findFields.call(this, locator)
|
|
937
1028
|
}
|
|
938
1029
|
|
|
939
1030
|
/**
|
|
@@ -941,7 +1032,26 @@ class Puppeteer extends Helper {
|
|
|
941
1032
|
*
|
|
942
1033
|
*/
|
|
943
1034
|
async grabWebElements(locator) {
|
|
944
|
-
|
|
1035
|
+
const elements = await this._locate(locator)
|
|
1036
|
+
return elements.map(element => new WebElement(element, this))
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* {{> grabWebElement }}
|
|
1041
|
+
*
|
|
1042
|
+
*/
|
|
1043
|
+
async grabWebElement(locator) {
|
|
1044
|
+
const elements = await this._locate(locator)
|
|
1045
|
+
if (elements.length === 0) {
|
|
1046
|
+
throw new ElementNotFound(locator, 'Element')
|
|
1047
|
+
}
|
|
1048
|
+
return new WebElement(elements[0], this)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async grabWebElement(locator) {
|
|
1052
|
+
const els = await this._locate(locator)
|
|
1053
|
+
assertElementExists(els, locator)
|
|
1054
|
+
return els[0]
|
|
945
1055
|
}
|
|
946
1056
|
|
|
947
1057
|
/**
|
|
@@ -955,17 +1065,17 @@ class Puppeteer extends Helper {
|
|
|
955
1065
|
* @param {number} [num=1]
|
|
956
1066
|
*/
|
|
957
1067
|
async switchToNextTab(num = 1) {
|
|
958
|
-
const pages = await this.browser.pages()
|
|
959
|
-
const index = pages.indexOf(this.page)
|
|
960
|
-
this.withinLocator = null
|
|
961
|
-
const page = pages[index + num]
|
|
1068
|
+
const pages = await this.browser.pages()
|
|
1069
|
+
const index = pages.indexOf(this.page)
|
|
1070
|
+
this.withinLocator = null
|
|
1071
|
+
const page = pages[index + num]
|
|
962
1072
|
|
|
963
1073
|
if (!page) {
|
|
964
|
-
throw new Error(`There is no ability to switch to next tab with offset ${num}`)
|
|
1074
|
+
throw new Error(`There is no ability to switch to next tab with offset ${num}`)
|
|
965
1075
|
}
|
|
966
1076
|
|
|
967
|
-
await this._setPage(page)
|
|
968
|
-
return this._waitForAction()
|
|
1077
|
+
await this._setPage(page)
|
|
1078
|
+
return this._waitForAction()
|
|
969
1079
|
}
|
|
970
1080
|
|
|
971
1081
|
/**
|
|
@@ -978,17 +1088,17 @@ class Puppeteer extends Helper {
|
|
|
978
1088
|
* @param {number} [num=1]
|
|
979
1089
|
*/
|
|
980
1090
|
async switchToPreviousTab(num = 1) {
|
|
981
|
-
const pages = await this.browser.pages()
|
|
982
|
-
const index = pages.indexOf(this.page)
|
|
983
|
-
this.withinLocator = null
|
|
984
|
-
const page = pages[index - num]
|
|
1091
|
+
const pages = await this.browser.pages()
|
|
1092
|
+
const index = pages.indexOf(this.page)
|
|
1093
|
+
this.withinLocator = null
|
|
1094
|
+
const page = pages[index - num]
|
|
985
1095
|
|
|
986
1096
|
if (!page) {
|
|
987
|
-
throw new Error(`There is no ability to switch to previous tab with offset ${num}`)
|
|
1097
|
+
throw new Error(`There is no ability to switch to previous tab with offset ${num}`)
|
|
988
1098
|
}
|
|
989
1099
|
|
|
990
|
-
await this._setPage(page)
|
|
991
|
-
return this._waitForAction()
|
|
1100
|
+
await this._setPage(page)
|
|
1101
|
+
return this._waitForAction()
|
|
992
1102
|
}
|
|
993
1103
|
|
|
994
1104
|
/**
|
|
@@ -999,10 +1109,10 @@ class Puppeteer extends Helper {
|
|
|
999
1109
|
* ```
|
|
1000
1110
|
*/
|
|
1001
1111
|
async closeCurrentTab() {
|
|
1002
|
-
const oldPage = this.page
|
|
1003
|
-
await this.switchToPreviousTab()
|
|
1004
|
-
await oldPage.close()
|
|
1005
|
-
return this._waitForAction()
|
|
1112
|
+
const oldPage = this.page
|
|
1113
|
+
await this.switchToPreviousTab()
|
|
1114
|
+
await oldPage.close()
|
|
1115
|
+
return this._waitForAction()
|
|
1006
1116
|
}
|
|
1007
1117
|
|
|
1008
1118
|
/**
|
|
@@ -1013,15 +1123,15 @@ class Puppeteer extends Helper {
|
|
|
1013
1123
|
* ```
|
|
1014
1124
|
*/
|
|
1015
1125
|
async closeOtherTabs() {
|
|
1016
|
-
const pages = await this.browser.pages()
|
|
1017
|
-
const otherPages = pages.filter(page => page !== this.page)
|
|
1126
|
+
const pages = await this.browser.pages()
|
|
1127
|
+
const otherPages = pages.filter(page => page !== this.page)
|
|
1018
1128
|
|
|
1019
|
-
let p = Promise.resolve()
|
|
1020
|
-
otherPages.forEach(
|
|
1021
|
-
p = p.then(() => page.close())
|
|
1022
|
-
})
|
|
1023
|
-
await p
|
|
1024
|
-
return this._waitForAction()
|
|
1129
|
+
let p = Promise.resolve()
|
|
1130
|
+
otherPages.forEach(page => {
|
|
1131
|
+
p = p.then(() => page.close())
|
|
1132
|
+
})
|
|
1133
|
+
await p
|
|
1134
|
+
return this._waitForAction()
|
|
1025
1135
|
}
|
|
1026
1136
|
|
|
1027
1137
|
/**
|
|
@@ -1032,16 +1142,16 @@ class Puppeteer extends Helper {
|
|
|
1032
1142
|
* ```
|
|
1033
1143
|
*/
|
|
1034
1144
|
async openNewTab() {
|
|
1035
|
-
await this._setPage(await this.browser.newPage())
|
|
1036
|
-
return this._waitForAction()
|
|
1145
|
+
await this._setPage(await this.browser.newPage())
|
|
1146
|
+
return this._waitForAction()
|
|
1037
1147
|
}
|
|
1038
1148
|
|
|
1039
1149
|
/**
|
|
1040
1150
|
* {{> grabNumberOfOpenTabs }}
|
|
1041
1151
|
*/
|
|
1042
1152
|
async grabNumberOfOpenTabs() {
|
|
1043
|
-
const pages = await this.browser.pages()
|
|
1044
|
-
return pages.length
|
|
1153
|
+
const pages = await this.browser.pages()
|
|
1154
|
+
return pages.length
|
|
1045
1155
|
}
|
|
1046
1156
|
|
|
1047
1157
|
/**
|
|
@@ -1049,14 +1159,14 @@ class Puppeteer extends Helper {
|
|
|
1049
1159
|
* {{ react }}
|
|
1050
1160
|
*/
|
|
1051
1161
|
async seeElement(locator) {
|
|
1052
|
-
let els = await this._locate(locator)
|
|
1053
|
-
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1162
|
+
let els = await this._locate(locator)
|
|
1163
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1054
1164
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1055
|
-
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1165
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1056
1166
|
try {
|
|
1057
|
-
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1167
|
+
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1058
1168
|
} catch (e) {
|
|
1059
|
-
dontSeeElementError(locator)
|
|
1169
|
+
dontSeeElementError(locator)
|
|
1060
1170
|
}
|
|
1061
1171
|
}
|
|
1062
1172
|
|
|
@@ -1065,14 +1175,14 @@ class Puppeteer extends Helper {
|
|
|
1065
1175
|
* {{ react }}
|
|
1066
1176
|
*/
|
|
1067
1177
|
async dontSeeElement(locator) {
|
|
1068
|
-
let els = await this._locate(locator)
|
|
1069
|
-
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1178
|
+
let els = await this._locate(locator)
|
|
1179
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1070
1180
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1071
|
-
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1181
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1072
1182
|
try {
|
|
1073
|
-
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1183
|
+
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1074
1184
|
} catch (e) {
|
|
1075
|
-
seeElementError(locator)
|
|
1185
|
+
seeElementError(locator)
|
|
1076
1186
|
}
|
|
1077
1187
|
}
|
|
1078
1188
|
|
|
@@ -1080,11 +1190,11 @@ class Puppeteer extends Helper {
|
|
|
1080
1190
|
* {{> seeElementInDOM }}
|
|
1081
1191
|
*/
|
|
1082
1192
|
async seeElementInDOM(locator) {
|
|
1083
|
-
const els = await this._locate(locator)
|
|
1193
|
+
const els = await this._locate(locator)
|
|
1084
1194
|
try {
|
|
1085
|
-
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1195
|
+
return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT'))
|
|
1086
1196
|
} catch (e) {
|
|
1087
|
-
dontSeeElementInDOMError(locator)
|
|
1197
|
+
dontSeeElementInDOMError(locator)
|
|
1088
1198
|
}
|
|
1089
1199
|
}
|
|
1090
1200
|
|
|
@@ -1092,11 +1202,11 @@ class Puppeteer extends Helper {
|
|
|
1092
1202
|
* {{> dontSeeElementInDOM }}
|
|
1093
1203
|
*/
|
|
1094
1204
|
async dontSeeElementInDOM(locator) {
|
|
1095
|
-
const els = await this._locate(locator)
|
|
1205
|
+
const els = await this._locate(locator)
|
|
1096
1206
|
try {
|
|
1097
|
-
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1207
|
+
return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT'))
|
|
1098
1208
|
} catch (e) {
|
|
1099
|
-
seeElementInDOMError(locator)
|
|
1209
|
+
seeElementInDOMError(locator)
|
|
1100
1210
|
}
|
|
1101
1211
|
}
|
|
1102
1212
|
|
|
@@ -1105,8 +1215,8 @@ class Puppeteer extends Helper {
|
|
|
1105
1215
|
*
|
|
1106
1216
|
* {{ react }}
|
|
1107
1217
|
*/
|
|
1108
|
-
async click(locator, context = null) {
|
|
1109
|
-
return proceedClick.call(this, locator, context)
|
|
1218
|
+
async click(locator = '//body', context = null) {
|
|
1219
|
+
return proceedClick.call(this, locator, context)
|
|
1110
1220
|
}
|
|
1111
1221
|
|
|
1112
1222
|
/**
|
|
@@ -1115,28 +1225,28 @@ class Puppeteer extends Helper {
|
|
|
1115
1225
|
* {{ react }}
|
|
1116
1226
|
*/
|
|
1117
1227
|
async forceClick(locator, context = null) {
|
|
1118
|
-
let matcher = await this.context
|
|
1228
|
+
let matcher = await this.context
|
|
1119
1229
|
if (context) {
|
|
1120
|
-
const els = await this._locate(context)
|
|
1121
|
-
assertElementExists(els, context)
|
|
1122
|
-
matcher = els[0]
|
|
1230
|
+
const els = await this._locate(context)
|
|
1231
|
+
assertElementExists(els, context)
|
|
1232
|
+
matcher = els[0]
|
|
1123
1233
|
}
|
|
1124
1234
|
|
|
1125
|
-
const els = await findClickable.call(this, matcher, locator)
|
|
1235
|
+
const els = await findClickable.call(this, matcher, locator)
|
|
1126
1236
|
if (context) {
|
|
1127
|
-
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
1237
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
1128
1238
|
} else {
|
|
1129
|
-
assertElementExists(els, locator, 'Clickable element')
|
|
1239
|
+
assertElementExists(els, locator, 'Clickable element')
|
|
1130
1240
|
}
|
|
1131
|
-
const elem = els[0]
|
|
1132
|
-
return this.executeScript(
|
|
1241
|
+
const elem = els[0]
|
|
1242
|
+
return this.executeScript(el => {
|
|
1133
1243
|
if (document.activeElement instanceof HTMLElement) {
|
|
1134
|
-
document.activeElement.blur()
|
|
1244
|
+
document.activeElement.blur()
|
|
1135
1245
|
}
|
|
1136
|
-
const event = document.createEvent('MouseEvent')
|
|
1137
|
-
event.initEvent('click', true, true)
|
|
1138
|
-
return el.dispatchEvent(event)
|
|
1139
|
-
}, elem)
|
|
1246
|
+
const event = document.createEvent('MouseEvent')
|
|
1247
|
+
event.initEvent('click', true, true)
|
|
1248
|
+
return el.dispatchEvent(event)
|
|
1249
|
+
}, elem)
|
|
1140
1250
|
}
|
|
1141
1251
|
|
|
1142
1252
|
/**
|
|
@@ -1145,7 +1255,7 @@ class Puppeteer extends Helper {
|
|
|
1145
1255
|
* {{ react }}
|
|
1146
1256
|
*/
|
|
1147
1257
|
async clickLink(locator, context = null) {
|
|
1148
|
-
return proceedClick.call(this, locator, context, { waitForNavigation: true })
|
|
1258
|
+
return proceedClick.call(this, locator, context, { waitForNavigation: true })
|
|
1149
1259
|
}
|
|
1150
1260
|
|
|
1151
1261
|
/**
|
|
@@ -1166,16 +1276,16 @@ class Puppeteer extends Helper {
|
|
|
1166
1276
|
* @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
|
|
1167
1277
|
*/
|
|
1168
1278
|
async handleDownloads(downloadPath = 'downloads') {
|
|
1169
|
-
downloadPath = path.join(global.output_dir, downloadPath)
|
|
1279
|
+
downloadPath = path.join(global.output_dir, downloadPath)
|
|
1170
1280
|
if (!fs.existsSync(downloadPath)) {
|
|
1171
|
-
fs.mkdirSync(downloadPath, '0777')
|
|
1281
|
+
fs.mkdirSync(downloadPath, '0777')
|
|
1172
1282
|
}
|
|
1173
|
-
fsExtra.emptyDirSync(downloadPath)
|
|
1283
|
+
fsExtra.emptyDirSync(downloadPath)
|
|
1174
1284
|
|
|
1175
1285
|
try {
|
|
1176
|
-
return this.page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
|
|
1286
|
+
return this.page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
|
|
1177
1287
|
} catch (e) {
|
|
1178
|
-
return this.page._client().send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
|
|
1288
|
+
return this.page._client().send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath })
|
|
1179
1289
|
}
|
|
1180
1290
|
}
|
|
1181
1291
|
|
|
@@ -1185,27 +1295,27 @@ class Puppeteer extends Helper {
|
|
|
1185
1295
|
* Please use `handleDownloads()` instead.
|
|
1186
1296
|
*/
|
|
1187
1297
|
async downloadFile(locator, customName) {
|
|
1188
|
-
let fileName
|
|
1189
|
-
await this.page.setRequestInterception(true)
|
|
1190
|
-
|
|
1191
|
-
const xRequest = await new Promise(
|
|
1192
|
-
this.page.on('request',
|
|
1193
|
-
console.log('rq', request, customName)
|
|
1194
|
-
const grabbedFileName = request.url().split('/')[request.url().split('/').length - 1]
|
|
1195
|
-
const fileExtension = request.url().split('/')[request.url().split('/').length - 1].split('.')[1]
|
|
1196
|
-
console.log('nm', customName, fileExtension)
|
|
1298
|
+
let fileName
|
|
1299
|
+
await this.page.setRequestInterception(true)
|
|
1300
|
+
|
|
1301
|
+
const xRequest = await new Promise(resolve => {
|
|
1302
|
+
this.page.on('request', request => {
|
|
1303
|
+
console.log('rq', request, customName)
|
|
1304
|
+
const grabbedFileName = request.url().split('/')[request.url().split('/').length - 1]
|
|
1305
|
+
const fileExtension = request.url().split('/')[request.url().split('/').length - 1].split('.')[1]
|
|
1306
|
+
console.log('nm', customName, fileExtension)
|
|
1197
1307
|
if (customName && path.extname(customName) !== fileExtension) {
|
|
1198
|
-
console.log('bypassing a request')
|
|
1199
|
-
request.continue()
|
|
1200
|
-
return
|
|
1308
|
+
console.log('bypassing a request')
|
|
1309
|
+
request.continue()
|
|
1310
|
+
return
|
|
1201
1311
|
}
|
|
1202
|
-
customName ? fileName = `${customName}.${fileExtension}` : fileName = grabbedFileName
|
|
1203
|
-
request.abort()
|
|
1204
|
-
resolve(request)
|
|
1205
|
-
})
|
|
1206
|
-
})
|
|
1312
|
+
customName ? (fileName = `${customName}.${fileExtension}`) : (fileName = grabbedFileName)
|
|
1313
|
+
request.abort()
|
|
1314
|
+
resolve(request)
|
|
1315
|
+
})
|
|
1316
|
+
})
|
|
1207
1317
|
|
|
1208
|
-
await this.click(locator)
|
|
1318
|
+
await this.click(locator)
|
|
1209
1319
|
|
|
1210
1320
|
const options = {
|
|
1211
1321
|
encoding: null,
|
|
@@ -1213,10 +1323,10 @@ class Puppeteer extends Helper {
|
|
|
1213
1323
|
uri: xRequest._url,
|
|
1214
1324
|
body: xRequest._postData,
|
|
1215
1325
|
headers: xRequest._headers,
|
|
1216
|
-
}
|
|
1326
|
+
}
|
|
1217
1327
|
|
|
1218
|
-
const cookies = await this.page.cookies()
|
|
1219
|
-
options.headers.Cookie = cookies.map(ck => `${ck.name}=${ck.value}`).join(';')
|
|
1328
|
+
const cookies = await this.page.cookies()
|
|
1329
|
+
options.headers.Cookie = cookies.map(ck => `${ck.name}=${ck.value}`).join(';')
|
|
1220
1330
|
|
|
1221
1331
|
const response = await axios({
|
|
1222
1332
|
method: options.method,
|
|
@@ -1224,24 +1334,26 @@ class Puppeteer extends Helper {
|
|
|
1224
1334
|
headers: options.headers,
|
|
1225
1335
|
responseType: 'arraybuffer',
|
|
1226
1336
|
onDownloadProgress(e) {
|
|
1227
|
-
console.log('+', e)
|
|
1337
|
+
console.log('+', e)
|
|
1228
1338
|
},
|
|
1229
|
-
})
|
|
1339
|
+
})
|
|
1230
1340
|
|
|
1231
|
-
const outputFile = path.join(`${global.output_dir}/${fileName}`)
|
|
1341
|
+
const outputFile = path.join(`${global.output_dir}/${fileName}`)
|
|
1232
1342
|
|
|
1233
1343
|
try {
|
|
1234
1344
|
await new Promise((resolve, reject) => {
|
|
1235
|
-
const wstream = fs.createWriteStream(outputFile)
|
|
1236
|
-
console.log(response)
|
|
1237
|
-
wstream.write(response.data)
|
|
1238
|
-
wstream.end()
|
|
1239
|
-
this.debug(`File is downloaded in ${outputFile}`)
|
|
1240
|
-
wstream.on('finish', () => {
|
|
1241
|
-
|
|
1242
|
-
|
|
1345
|
+
const wstream = fs.createWriteStream(outputFile)
|
|
1346
|
+
console.log(response)
|
|
1347
|
+
wstream.write(response.data)
|
|
1348
|
+
wstream.end()
|
|
1349
|
+
this.debug(`File is downloaded in ${outputFile}`)
|
|
1350
|
+
wstream.on('finish', () => {
|
|
1351
|
+
resolve(fileName)
|
|
1352
|
+
})
|
|
1353
|
+
wstream.on('error', reject)
|
|
1354
|
+
})
|
|
1243
1355
|
} catch (error) {
|
|
1244
|
-
throw new Error(`There is something wrong with downloaded file. ${error}`)
|
|
1356
|
+
throw new Error(`There is something wrong with downloaded file. ${error}`)
|
|
1245
1357
|
}
|
|
1246
1358
|
}
|
|
1247
1359
|
|
|
@@ -1251,7 +1363,7 @@ class Puppeteer extends Helper {
|
|
|
1251
1363
|
* {{ react }}
|
|
1252
1364
|
*/
|
|
1253
1365
|
async doubleClick(locator, context = null) {
|
|
1254
|
-
return proceedClick.call(this, locator, context, { clickCount: 2 })
|
|
1366
|
+
return proceedClick.call(this, locator, context, { clickCount: 2 })
|
|
1255
1367
|
}
|
|
1256
1368
|
|
|
1257
1369
|
/**
|
|
@@ -1260,20 +1372,70 @@ class Puppeteer extends Helper {
|
|
|
1260
1372
|
* {{ react }}
|
|
1261
1373
|
*/
|
|
1262
1374
|
async rightClick(locator, context = null) {
|
|
1263
|
-
return proceedClick.call(this, locator, context, { button: 'right' })
|
|
1375
|
+
return proceedClick.call(this, locator, context, { button: 'right' })
|
|
1376
|
+
}
|
|
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()
|
|
1264
1419
|
}
|
|
1265
1420
|
|
|
1266
1421
|
/**
|
|
1267
1422
|
* {{> checkOption }}
|
|
1268
1423
|
*/
|
|
1269
1424
|
async checkOption(field, context = null) {
|
|
1270
|
-
const elm = await this._locateCheckable(field, context)
|
|
1271
|
-
|
|
1272
|
-
.
|
|
1273
|
-
|
|
1425
|
+
const elm = await this._locateCheckable(field, context)
|
|
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
|
+
|
|
1274
1436
|
if (!curentlyChecked) {
|
|
1275
|
-
await elm.click()
|
|
1276
|
-
return this._waitForAction()
|
|
1437
|
+
await elm.click()
|
|
1438
|
+
return this._waitForAction()
|
|
1277
1439
|
}
|
|
1278
1440
|
}
|
|
1279
1441
|
|
|
@@ -1281,13 +1443,20 @@ class Puppeteer extends Helper {
|
|
|
1281
1443
|
* {{> uncheckOption }}
|
|
1282
1444
|
*/
|
|
1283
1445
|
async uncheckOption(field, context = null) {
|
|
1284
|
-
const elm = await this._locateCheckable(field, context)
|
|
1285
|
-
|
|
1286
|
-
.
|
|
1287
|
-
|
|
1446
|
+
const elm = await this._locateCheckable(field, context)
|
|
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
|
+
|
|
1288
1457
|
if (curentlyChecked) {
|
|
1289
|
-
await elm.click()
|
|
1290
|
-
return this._waitForAction()
|
|
1458
|
+
await elm.click()
|
|
1459
|
+
return this._waitForAction()
|
|
1291
1460
|
}
|
|
1292
1461
|
}
|
|
1293
1462
|
|
|
@@ -1295,62 +1464,62 @@ class Puppeteer extends Helper {
|
|
|
1295
1464
|
* {{> seeCheckboxIsChecked }}
|
|
1296
1465
|
*/
|
|
1297
1466
|
async seeCheckboxIsChecked(field) {
|
|
1298
|
-
return proceedIsChecked.call(this, 'assert', field)
|
|
1467
|
+
return proceedIsChecked.call(this, 'assert', field)
|
|
1299
1468
|
}
|
|
1300
1469
|
|
|
1301
1470
|
/**
|
|
1302
1471
|
* {{> dontSeeCheckboxIsChecked }}
|
|
1303
1472
|
*/
|
|
1304
1473
|
async dontSeeCheckboxIsChecked(field) {
|
|
1305
|
-
return proceedIsChecked.call(this, 'negate', field)
|
|
1474
|
+
return proceedIsChecked.call(this, 'negate', field)
|
|
1306
1475
|
}
|
|
1307
1476
|
|
|
1308
1477
|
/**
|
|
1309
1478
|
* {{> pressKeyDown }}
|
|
1310
1479
|
*/
|
|
1311
1480
|
async pressKeyDown(key) {
|
|
1312
|
-
key = getNormalizedKey.call(this, key)
|
|
1313
|
-
await this.page.keyboard.down(key)
|
|
1314
|
-
return this._waitForAction()
|
|
1481
|
+
key = getNormalizedKey.call(this, key)
|
|
1482
|
+
await this.page.keyboard.down(key)
|
|
1483
|
+
return this._waitForAction()
|
|
1315
1484
|
}
|
|
1316
1485
|
|
|
1317
1486
|
/**
|
|
1318
1487
|
* {{> pressKeyUp }}
|
|
1319
1488
|
*/
|
|
1320
1489
|
async pressKeyUp(key) {
|
|
1321
|
-
key = getNormalizedKey.call(this, key)
|
|
1322
|
-
await this.page.keyboard.up(key)
|
|
1323
|
-
return this._waitForAction()
|
|
1490
|
+
key = getNormalizedKey.call(this, key)
|
|
1491
|
+
await this.page.keyboard.up(key)
|
|
1492
|
+
return this._waitForAction()
|
|
1324
1493
|
}
|
|
1325
1494
|
|
|
1326
1495
|
/**
|
|
1327
|
-
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([
|
|
1496
|
+
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([puppeteer/puppeteer#1313](https://github.com/puppeteer/puppeteer/issues/1313)).
|
|
1328
1497
|
*
|
|
1329
1498
|
* {{> pressKeyWithKeyNormalization }}
|
|
1330
1499
|
*/
|
|
1331
1500
|
async pressKey(key) {
|
|
1332
|
-
const modifiers = []
|
|
1501
|
+
const modifiers = []
|
|
1333
1502
|
if (Array.isArray(key)) {
|
|
1334
1503
|
for (let k of key) {
|
|
1335
|
-
k = getNormalizedKey.call(this, k)
|
|
1504
|
+
k = getNormalizedKey.call(this, k)
|
|
1336
1505
|
if (isModifierKey(k)) {
|
|
1337
|
-
modifiers.push(k)
|
|
1506
|
+
modifiers.push(k)
|
|
1338
1507
|
} else {
|
|
1339
|
-
key = k
|
|
1340
|
-
break
|
|
1508
|
+
key = k
|
|
1509
|
+
break
|
|
1341
1510
|
}
|
|
1342
1511
|
}
|
|
1343
1512
|
} else {
|
|
1344
|
-
key = getNormalizedKey.call(this, key)
|
|
1513
|
+
key = getNormalizedKey.call(this, key)
|
|
1345
1514
|
}
|
|
1346
1515
|
for (const modifier of modifiers) {
|
|
1347
|
-
await this.page.keyboard.down(modifier)
|
|
1516
|
+
await this.page.keyboard.down(modifier)
|
|
1348
1517
|
}
|
|
1349
|
-
await this.page.keyboard.press(key)
|
|
1518
|
+
await this.page.keyboard.press(key)
|
|
1350
1519
|
for (const modifier of modifiers) {
|
|
1351
|
-
await this.page.keyboard.up(modifier)
|
|
1520
|
+
await this.page.keyboard.up(modifier)
|
|
1352
1521
|
}
|
|
1353
|
-
return this._waitForAction()
|
|
1522
|
+
return this._waitForAction()
|
|
1354
1523
|
}
|
|
1355
1524
|
|
|
1356
1525
|
/**
|
|
@@ -1358,13 +1527,13 @@ class Puppeteer extends Helper {
|
|
|
1358
1527
|
*/
|
|
1359
1528
|
async type(keys, delay = null) {
|
|
1360
1529
|
if (!Array.isArray(keys)) {
|
|
1361
|
-
keys = keys.toString()
|
|
1362
|
-
keys = keys.split('')
|
|
1530
|
+
keys = keys.toString()
|
|
1531
|
+
keys = keys.split('')
|
|
1363
1532
|
}
|
|
1364
1533
|
|
|
1365
1534
|
for (const key of keys) {
|
|
1366
|
-
await this.page.keyboard.press(key)
|
|
1367
|
-
if (delay) await this.wait(delay / 1000)
|
|
1535
|
+
await this.page.keyboard.press(key)
|
|
1536
|
+
if (delay) await this.wait(delay / 1000)
|
|
1368
1537
|
}
|
|
1369
1538
|
}
|
|
1370
1539
|
|
|
@@ -1373,28 +1542,28 @@ class Puppeteer extends Helper {
|
|
|
1373
1542
|
* {{ react }}
|
|
1374
1543
|
*/
|
|
1375
1544
|
async fillField(field, value) {
|
|
1376
|
-
const els = await findVisibleFields.call(this, field)
|
|
1377
|
-
assertElementExists(els, field, 'Field')
|
|
1378
|
-
const el = els[0]
|
|
1379
|
-
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1380
|
-
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1545
|
+
const els = await findVisibleFields.call(this, field)
|
|
1546
|
+
assertElementExists(els, field, 'Field')
|
|
1547
|
+
const el = els[0]
|
|
1548
|
+
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1549
|
+
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1381
1550
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1382
|
-
await this._evaluateHandeInContext(el => el.value = '', el)
|
|
1551
|
+
await this._evaluateHandeInContext(el => (el.value = ''), el)
|
|
1383
1552
|
} else if (editable) {
|
|
1384
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el)
|
|
1553
|
+
await this._evaluateHandeInContext(el => (el.innerHTML = ''), el)
|
|
1385
1554
|
}
|
|
1386
1555
|
|
|
1387
|
-
highlightActiveElement.call(this, el, await this._getContext())
|
|
1388
|
-
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
1556
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
1557
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
1389
1558
|
|
|
1390
|
-
return this._waitForAction()
|
|
1559
|
+
return this._waitForAction()
|
|
1391
1560
|
}
|
|
1392
1561
|
|
|
1393
1562
|
/**
|
|
1394
1563
|
* {{> clearField }}
|
|
1395
1564
|
*/
|
|
1396
1565
|
async clearField(field) {
|
|
1397
|
-
return this.fillField(field, '')
|
|
1566
|
+
return this.fillField(field, '')
|
|
1398
1567
|
}
|
|
1399
1568
|
|
|
1400
1569
|
/**
|
|
@@ -1403,28 +1572,28 @@ class Puppeteer extends Helper {
|
|
|
1403
1572
|
* {{ react }}
|
|
1404
1573
|
*/
|
|
1405
1574
|
async appendField(field, value) {
|
|
1406
|
-
const els = await findVisibleFields.call(this, field)
|
|
1407
|
-
assertElementExists(els, field, 'Field')
|
|
1408
|
-
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
1409
|
-
await els[0].press('End')
|
|
1410
|
-
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
1411
|
-
return this._waitForAction()
|
|
1575
|
+
const els = await findVisibleFields.call(this, field)
|
|
1576
|
+
assertElementExists(els, field, 'Field')
|
|
1577
|
+
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
1578
|
+
await els[0].press('End')
|
|
1579
|
+
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
1580
|
+
return this._waitForAction()
|
|
1412
1581
|
}
|
|
1413
1582
|
|
|
1414
1583
|
/**
|
|
1415
1584
|
* {{> seeInField }}
|
|
1416
1585
|
*/
|
|
1417
1586
|
async seeInField(field, value) {
|
|
1418
|
-
const _value =
|
|
1419
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
1587
|
+
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1588
|
+
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
1420
1589
|
}
|
|
1421
1590
|
|
|
1422
1591
|
/**
|
|
1423
1592
|
* {{> dontSeeInField }}
|
|
1424
1593
|
*/
|
|
1425
1594
|
async dontSeeInField(field, value) {
|
|
1426
|
-
const _value =
|
|
1427
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
1595
|
+
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1596
|
+
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
1428
1597
|
}
|
|
1429
1598
|
|
|
1430
1599
|
/**
|
|
@@ -1433,48 +1602,48 @@ class Puppeteer extends Helper {
|
|
|
1433
1602
|
* {{> attachFile }}
|
|
1434
1603
|
*/
|
|
1435
1604
|
async attachFile(locator, pathToFile) {
|
|
1436
|
-
const file = path.join(global.codecept_dir, pathToFile)
|
|
1605
|
+
const file = path.join(global.codecept_dir, pathToFile)
|
|
1437
1606
|
|
|
1438
1607
|
if (!fileExists(file)) {
|
|
1439
|
-
throw new Error(`File at ${file} can not be found on local system`)
|
|
1608
|
+
throw new Error(`File at ${file} can not be found on local system`)
|
|
1440
1609
|
}
|
|
1441
|
-
const els = await findFields.call(this, locator)
|
|
1442
|
-
assertElementExists(els, locator, 'Field')
|
|
1443
|
-
await els[0].uploadFile(file)
|
|
1444
|
-
return this._waitForAction()
|
|
1610
|
+
const els = await findFields.call(this, locator)
|
|
1611
|
+
assertElementExists(els, locator, 'Field')
|
|
1612
|
+
await els[0].uploadFile(file)
|
|
1613
|
+
return this._waitForAction()
|
|
1445
1614
|
}
|
|
1446
1615
|
|
|
1447
1616
|
/**
|
|
1448
1617
|
* {{> selectOption }}
|
|
1449
1618
|
*/
|
|
1450
1619
|
async selectOption(select, option) {
|
|
1451
|
-
const els = await findVisibleFields.call(this, select)
|
|
1452
|
-
assertElementExists(els, select, 'Selectable field')
|
|
1453
|
-
const el = els[0]
|
|
1620
|
+
const els = await findVisibleFields.call(this, select)
|
|
1621
|
+
assertElementExists(els, select, 'Selectable field')
|
|
1622
|
+
const el = els[0]
|
|
1454
1623
|
if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
|
|
1455
|
-
throw new Error('Element is not <select>')
|
|
1624
|
+
throw new Error('Element is not <select>')
|
|
1456
1625
|
}
|
|
1457
|
-
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
1458
|
-
if (!Array.isArray(option)) option = [option]
|
|
1626
|
+
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
1627
|
+
if (!Array.isArray(option)) option = [option]
|
|
1459
1628
|
|
|
1460
1629
|
for (const key in option) {
|
|
1461
|
-
const opt = xpathLocator.literal(option[key])
|
|
1462
|
-
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
|
|
1630
|
+
const opt = xpathLocator.literal(option[key])
|
|
1631
|
+
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
|
|
1463
1632
|
if (optEl.length) {
|
|
1464
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0])
|
|
1465
|
-
continue
|
|
1633
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1634
|
+
continue
|
|
1466
1635
|
}
|
|
1467
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
|
|
1636
|
+
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
|
|
1468
1637
|
if (optEl.length) {
|
|
1469
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0])
|
|
1638
|
+
this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
|
|
1470
1639
|
}
|
|
1471
1640
|
}
|
|
1472
|
-
await this._evaluateHandeInContext(
|
|
1473
|
-
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1474
|
-
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1475
|
-
}, el)
|
|
1641
|
+
await this._evaluateHandeInContext(element => {
|
|
1642
|
+
element.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1643
|
+
element.dispatchEvent(new Event('change', { bubbles: true }))
|
|
1644
|
+
}, el)
|
|
1476
1645
|
|
|
1477
|
-
return this._waitForAction()
|
|
1646
|
+
return this._waitForAction()
|
|
1478
1647
|
}
|
|
1479
1648
|
|
|
1480
1649
|
/**
|
|
@@ -1482,40 +1651,40 @@ class Puppeteer extends Helper {
|
|
|
1482
1651
|
* {{ react }}
|
|
1483
1652
|
*/
|
|
1484
1653
|
async grabNumberOfVisibleElements(locator) {
|
|
1485
|
-
let els = await this._locate(locator)
|
|
1486
|
-
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1654
|
+
let els = await this._locate(locator)
|
|
1655
|
+
els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
|
|
1487
1656
|
// Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
|
|
1488
|
-
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1657
|
+
els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
|
|
1489
1658
|
|
|
1490
|
-
return els.filter(v => v).length
|
|
1659
|
+
return els.filter(v => v).length
|
|
1491
1660
|
}
|
|
1492
1661
|
|
|
1493
1662
|
/**
|
|
1494
1663
|
* {{> seeInCurrentUrl }}
|
|
1495
1664
|
*/
|
|
1496
1665
|
async seeInCurrentUrl(url) {
|
|
1497
|
-
stringIncludes('url').assert(url, await this._getPageUrl())
|
|
1666
|
+
stringIncludes('url').assert(url, await this._getPageUrl())
|
|
1498
1667
|
}
|
|
1499
1668
|
|
|
1500
1669
|
/**
|
|
1501
1670
|
* {{> dontSeeInCurrentUrl }}
|
|
1502
1671
|
*/
|
|
1503
1672
|
async dontSeeInCurrentUrl(url) {
|
|
1504
|
-
stringIncludes('url').negate(url, await this._getPageUrl())
|
|
1673
|
+
stringIncludes('url').negate(url, await this._getPageUrl())
|
|
1505
1674
|
}
|
|
1506
1675
|
|
|
1507
1676
|
/**
|
|
1508
1677
|
* {{> seeCurrentUrlEquals }}
|
|
1509
1678
|
*/
|
|
1510
1679
|
async seeCurrentUrlEquals(url) {
|
|
1511
|
-
urlEquals(this.options.url).assert(url, await this._getPageUrl())
|
|
1680
|
+
urlEquals(this.options.url).assert(url, await this._getPageUrl())
|
|
1512
1681
|
}
|
|
1513
1682
|
|
|
1514
1683
|
/**
|
|
1515
1684
|
* {{> dontSeeCurrentUrlEquals }}
|
|
1516
1685
|
*/
|
|
1517
1686
|
async dontSeeCurrentUrlEquals(url) {
|
|
1518
|
-
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
1687
|
+
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
1519
1688
|
}
|
|
1520
1689
|
|
|
1521
1690
|
/**
|
|
@@ -1524,14 +1693,14 @@ class Puppeteer extends Helper {
|
|
|
1524
1693
|
* {{ react }}
|
|
1525
1694
|
*/
|
|
1526
1695
|
async see(text, context = null) {
|
|
1527
|
-
return proceedSee.call(this, 'assert', text, context)
|
|
1696
|
+
return proceedSee.call(this, 'assert', text, context)
|
|
1528
1697
|
}
|
|
1529
1698
|
|
|
1530
1699
|
/**
|
|
1531
1700
|
* {{> seeTextEquals }}
|
|
1532
1701
|
*/
|
|
1533
1702
|
async seeTextEquals(text, context = null) {
|
|
1534
|
-
return proceedSee.call(this, 'assert', text, context, true)
|
|
1703
|
+
return proceedSee.call(this, 'assert', text, context, true)
|
|
1535
1704
|
}
|
|
1536
1705
|
|
|
1537
1706
|
/**
|
|
@@ -1540,14 +1709,14 @@ class Puppeteer extends Helper {
|
|
|
1540
1709
|
* {{ react }}
|
|
1541
1710
|
*/
|
|
1542
1711
|
async dontSee(text, context = null) {
|
|
1543
|
-
return proceedSee.call(this, 'negate', text, context)
|
|
1712
|
+
return proceedSee.call(this, 'negate', text, context)
|
|
1544
1713
|
}
|
|
1545
1714
|
|
|
1546
1715
|
/**
|
|
1547
1716
|
* {{> grabSource }}
|
|
1548
1717
|
*/
|
|
1549
1718
|
async grabSource() {
|
|
1550
|
-
return this.page.content()
|
|
1719
|
+
return this.page.content()
|
|
1551
1720
|
}
|
|
1552
1721
|
|
|
1553
1722
|
/**
|
|
@@ -1560,32 +1729,32 @@ class Puppeteer extends Helper {
|
|
|
1560
1729
|
* @return {Promise<any[]>}
|
|
1561
1730
|
*/
|
|
1562
1731
|
async grabBrowserLogs() {
|
|
1563
|
-
const logs = consoleLogStore.entries
|
|
1564
|
-
consoleLogStore.clear()
|
|
1565
|
-
return logs
|
|
1732
|
+
const logs = consoleLogStore.entries
|
|
1733
|
+
consoleLogStore.clear()
|
|
1734
|
+
return logs
|
|
1566
1735
|
}
|
|
1567
1736
|
|
|
1568
1737
|
/**
|
|
1569
1738
|
* {{> grabCurrentUrl }}
|
|
1570
1739
|
*/
|
|
1571
1740
|
async grabCurrentUrl() {
|
|
1572
|
-
return this._getPageUrl()
|
|
1741
|
+
return this._getPageUrl()
|
|
1573
1742
|
}
|
|
1574
1743
|
|
|
1575
1744
|
/**
|
|
1576
1745
|
* {{> seeInSource }}
|
|
1577
1746
|
*/
|
|
1578
1747
|
async seeInSource(text) {
|
|
1579
|
-
const source = await this.page.content()
|
|
1580
|
-
stringIncludes('HTML source of a page').assert(text, source)
|
|
1748
|
+
const source = await this.page.content()
|
|
1749
|
+
stringIncludes('HTML source of a page').assert(text, source)
|
|
1581
1750
|
}
|
|
1582
1751
|
|
|
1583
1752
|
/**
|
|
1584
1753
|
* {{> dontSeeInSource }}
|
|
1585
1754
|
*/
|
|
1586
1755
|
async dontSeeInSource(text) {
|
|
1587
|
-
const source = await this.page.content()
|
|
1588
|
-
stringIncludes('HTML source of a page').negate(text, source)
|
|
1756
|
+
const source = await this.page.content()
|
|
1757
|
+
stringIncludes('HTML source of a page').negate(text, source)
|
|
1589
1758
|
}
|
|
1590
1759
|
|
|
1591
1760
|
/**
|
|
@@ -1594,8 +1763,8 @@ class Puppeteer extends Helper {
|
|
|
1594
1763
|
* {{ react }}
|
|
1595
1764
|
*/
|
|
1596
1765
|
async seeNumberOfElements(locator, num) {
|
|
1597
|
-
const elements = await this._locate(locator)
|
|
1598
|
-
return equals(`expected number of elements (${
|
|
1766
|
+
const elements = await this._locate(locator)
|
|
1767
|
+
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
|
|
1599
1768
|
}
|
|
1600
1769
|
|
|
1601
1770
|
/**
|
|
@@ -1604,8 +1773,8 @@ class Puppeteer extends Helper {
|
|
|
1604
1773
|
* {{ react }}
|
|
1605
1774
|
*/
|
|
1606
1775
|
async seeNumberOfVisibleElements(locator, num) {
|
|
1607
|
-
const res = await this.grabNumberOfVisibleElements(locator)
|
|
1608
|
-
return equals(`expected number of visible elements (${
|
|
1776
|
+
const res = await this.grabNumberOfVisibleElements(locator)
|
|
1777
|
+
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
|
|
1609
1778
|
}
|
|
1610
1779
|
|
|
1611
1780
|
/**
|
|
@@ -1613,9 +1782,9 @@ class Puppeteer extends Helper {
|
|
|
1613
1782
|
*/
|
|
1614
1783
|
async setCookie(cookie) {
|
|
1615
1784
|
if (Array.isArray(cookie)) {
|
|
1616
|
-
return this.page.setCookie(...cookie)
|
|
1785
|
+
return this.page.setCookie(...cookie)
|
|
1617
1786
|
}
|
|
1618
|
-
return this.page.setCookie(cookie)
|
|
1787
|
+
return this.page.setCookie(cookie)
|
|
1619
1788
|
}
|
|
1620
1789
|
|
|
1621
1790
|
/**
|
|
@@ -1623,16 +1792,16 @@ class Puppeteer extends Helper {
|
|
|
1623
1792
|
*
|
|
1624
1793
|
*/
|
|
1625
1794
|
async seeCookie(name) {
|
|
1626
|
-
const cookies = await this.page.cookies()
|
|
1627
|
-
empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
|
|
1795
|
+
const cookies = await this.page.cookies()
|
|
1796
|
+
empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name))
|
|
1628
1797
|
}
|
|
1629
1798
|
|
|
1630
1799
|
/**
|
|
1631
1800
|
* {{> dontSeeCookie }}
|
|
1632
1801
|
*/
|
|
1633
1802
|
async dontSeeCookie(name) {
|
|
1634
|
-
const cookies = await this.page.cookies()
|
|
1635
|
-
empty(`cookie ${name} to be set`).assert(cookies.filter(c => c.name === name))
|
|
1803
|
+
const cookies = await this.page.cookies()
|
|
1804
|
+
empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name))
|
|
1636
1805
|
}
|
|
1637
1806
|
|
|
1638
1807
|
/**
|
|
@@ -1641,10 +1810,10 @@ class Puppeteer extends Helper {
|
|
|
1641
1810
|
* Returns cookie in JSON format. If name not passed returns all cookies for this domain.
|
|
1642
1811
|
*/
|
|
1643
1812
|
async grabCookie(name) {
|
|
1644
|
-
const cookies = await this.page.cookies()
|
|
1645
|
-
if (!name) return cookies
|
|
1646
|
-
const cookie = cookies.filter(c => c.name === name)
|
|
1647
|
-
if (cookie[0]) return cookie[0]
|
|
1813
|
+
const cookies = await this.page.cookies()
|
|
1814
|
+
if (!name) return cookies
|
|
1815
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1816
|
+
if (cookie[0]) return cookie[0]
|
|
1648
1817
|
}
|
|
1649
1818
|
|
|
1650
1819
|
/**
|
|
@@ -1652,44 +1821,47 @@ class Puppeteer extends Helper {
|
|
|
1652
1821
|
*/
|
|
1653
1822
|
async waitForCookie(name, sec) {
|
|
1654
1823
|
// by default, we will retry 3 times
|
|
1655
|
-
let retries = 3
|
|
1656
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
1824
|
+
let retries = 3
|
|
1825
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
1657
1826
|
|
|
1658
1827
|
if (sec) {
|
|
1659
|
-
retries = sec
|
|
1828
|
+
retries = sec
|
|
1660
1829
|
} else {
|
|
1661
|
-
retries = Math.ceil(waitTimeout / 1000) - 1
|
|
1830
|
+
retries = Math.ceil(waitTimeout / 1000) - 1
|
|
1662
1831
|
}
|
|
1663
1832
|
|
|
1664
|
-
return promiseRetry(
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1833
|
+
return promiseRetry(
|
|
1834
|
+
async (retry, number) => {
|
|
1835
|
+
const _grabCookie = async name => {
|
|
1836
|
+
const cookies = await this.page.cookies()
|
|
1837
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1838
|
+
if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`)
|
|
1839
|
+
}
|
|
1670
1840
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1841
|
+
this.debugSection('Wait for cookie: ', name)
|
|
1842
|
+
if (number > 1) this.debugSection('Retrying... Attempt #', number)
|
|
1673
1843
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1844
|
+
try {
|
|
1845
|
+
await _grabCookie(name)
|
|
1846
|
+
} catch (e) {
|
|
1847
|
+
retry(e)
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
{ retries, maxTimeout: 1000 },
|
|
1851
|
+
)
|
|
1680
1852
|
}
|
|
1681
1853
|
|
|
1682
1854
|
/**
|
|
1683
1855
|
* {{> clearCookie }}
|
|
1684
1856
|
*/
|
|
1685
1857
|
async clearCookie(name) {
|
|
1686
|
-
const cookies = await this.page.cookies()
|
|
1858
|
+
const cookies = await this.page.cookies()
|
|
1687
1859
|
if (!name) {
|
|
1688
|
-
return this.page.deleteCookie.apply(this.page, cookies)
|
|
1860
|
+
return this.page.deleteCookie.apply(this.page, cookies)
|
|
1689
1861
|
}
|
|
1690
|
-
const cookie = cookies.filter(c => c.name === name)
|
|
1691
|
-
if (!cookie[0]) return
|
|
1692
|
-
return this.page.deleteCookie(cookie[0])
|
|
1862
|
+
const cookie = cookies.filter(c => c.name === name)
|
|
1863
|
+
if (!cookie[0]) return
|
|
1864
|
+
return this.page.deleteCookie(cookie[0])
|
|
1693
1865
|
}
|
|
1694
1866
|
|
|
1695
1867
|
/**
|
|
@@ -1698,11 +1870,11 @@ class Puppeteer extends Helper {
|
|
|
1698
1870
|
* {{> executeScript }}
|
|
1699
1871
|
*/
|
|
1700
1872
|
async executeScript(...args) {
|
|
1701
|
-
let context = await this._getContext()
|
|
1873
|
+
let context = await this._getContext()
|
|
1702
1874
|
if (this.context && this.context.constructor.name === 'CdpFrame') {
|
|
1703
|
-
context = this.context
|
|
1875
|
+
context = this.context // switching to iframe context
|
|
1704
1876
|
}
|
|
1705
|
-
return context.evaluate.apply(context, args)
|
|
1877
|
+
return context.evaluate.apply(context, args)
|
|
1706
1878
|
}
|
|
1707
1879
|
|
|
1708
1880
|
/**
|
|
@@ -1711,16 +1883,16 @@ class Puppeteer extends Helper {
|
|
|
1711
1883
|
*/
|
|
1712
1884
|
async executeAsyncScript(...args) {
|
|
1713
1885
|
const asyncFn = function () {
|
|
1714
|
-
const args = Array.from(arguments)
|
|
1715
|
-
const fn = eval(`(${args.shift()})`)
|
|
1716
|
-
return new Promise(
|
|
1717
|
-
args.push(done)
|
|
1718
|
-
fn.apply(null, args)
|
|
1719
|
-
})
|
|
1720
|
-
}
|
|
1721
|
-
args[0] = args[0].toString()
|
|
1722
|
-
args.unshift(asyncFn)
|
|
1723
|
-
return this.page.evaluate.apply(this.page, args)
|
|
1886
|
+
const args = Array.from(arguments)
|
|
1887
|
+
const fn = eval(`(${args.shift()})`)
|
|
1888
|
+
return new Promise(done => {
|
|
1889
|
+
args.push(done)
|
|
1890
|
+
fn.apply(null, args)
|
|
1891
|
+
})
|
|
1892
|
+
}
|
|
1893
|
+
args[0] = args[0].toString()
|
|
1894
|
+
args.unshift(asyncFn)
|
|
1895
|
+
return this.page.evaluate.apply(this.page, args)
|
|
1724
1896
|
}
|
|
1725
1897
|
|
|
1726
1898
|
/**
|
|
@@ -1728,12 +1900,12 @@ class Puppeteer extends Helper {
|
|
|
1728
1900
|
* {{ react }}
|
|
1729
1901
|
*/
|
|
1730
1902
|
async grabTextFromAll(locator) {
|
|
1731
|
-
const els = await this._locate(locator)
|
|
1732
|
-
const texts = []
|
|
1903
|
+
const els = await this._locate(locator)
|
|
1904
|
+
const texts = []
|
|
1733
1905
|
for (const el of els) {
|
|
1734
|
-
texts.push(await (await el.getProperty('innerText')).jsonValue())
|
|
1906
|
+
texts.push(await (await el.getProperty('innerText')).jsonValue())
|
|
1735
1907
|
}
|
|
1736
|
-
return texts
|
|
1908
|
+
return texts
|
|
1737
1909
|
}
|
|
1738
1910
|
|
|
1739
1911
|
/**
|
|
@@ -1741,60 +1913,60 @@ class Puppeteer extends Helper {
|
|
|
1741
1913
|
* {{ react }}
|
|
1742
1914
|
*/
|
|
1743
1915
|
async grabTextFrom(locator) {
|
|
1744
|
-
const texts = await this.grabTextFromAll(locator)
|
|
1745
|
-
assertElementExists(texts, locator)
|
|
1916
|
+
const texts = await this.grabTextFromAll(locator)
|
|
1917
|
+
assertElementExists(texts, locator)
|
|
1746
1918
|
if (texts.length > 1) {
|
|
1747
|
-
this.debugSection('GrabText', `Using first element out of ${texts.length}`)
|
|
1919
|
+
this.debugSection('GrabText', `Using first element out of ${texts.length}`)
|
|
1748
1920
|
}
|
|
1749
1921
|
|
|
1750
|
-
return texts[0]
|
|
1922
|
+
return texts[0]
|
|
1751
1923
|
}
|
|
1752
1924
|
|
|
1753
1925
|
/**
|
|
1754
1926
|
* {{> grabValueFromAll }}
|
|
1755
1927
|
*/
|
|
1756
1928
|
async grabValueFromAll(locator) {
|
|
1757
|
-
const els = await findFields.call(this, locator)
|
|
1758
|
-
const values = []
|
|
1929
|
+
const els = await findFields.call(this, locator)
|
|
1930
|
+
const values = []
|
|
1759
1931
|
for (const el of els) {
|
|
1760
|
-
values.push(await (await el.getProperty('value')).jsonValue())
|
|
1932
|
+
values.push(await (await el.getProperty('value')).jsonValue())
|
|
1761
1933
|
}
|
|
1762
|
-
return values
|
|
1934
|
+
return values
|
|
1763
1935
|
}
|
|
1764
1936
|
|
|
1765
1937
|
/**
|
|
1766
1938
|
* {{> grabValueFrom }}
|
|
1767
1939
|
*/
|
|
1768
1940
|
async grabValueFrom(locator) {
|
|
1769
|
-
const values = await this.grabValueFromAll(locator)
|
|
1770
|
-
assertElementExists(values, locator)
|
|
1941
|
+
const values = await this.grabValueFromAll(locator)
|
|
1942
|
+
assertElementExists(values, locator)
|
|
1771
1943
|
if (values.length > 1) {
|
|
1772
|
-
this.debugSection('GrabValue', `Using first element out of ${values.length}`)
|
|
1944
|
+
this.debugSection('GrabValue', `Using first element out of ${values.length}`)
|
|
1773
1945
|
}
|
|
1774
1946
|
|
|
1775
|
-
return values[0]
|
|
1947
|
+
return values[0]
|
|
1776
1948
|
}
|
|
1777
1949
|
|
|
1778
1950
|
/**
|
|
1779
1951
|
* {{> grabHTMLFromAll }}
|
|
1780
1952
|
*/
|
|
1781
1953
|
async grabHTMLFromAll(locator) {
|
|
1782
|
-
const els = await this._locate(locator)
|
|
1783
|
-
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML
|
|
1784
|
-
return values
|
|
1954
|
+
const els = await this._locate(locator)
|
|
1955
|
+
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML)))
|
|
1956
|
+
return values
|
|
1785
1957
|
}
|
|
1786
1958
|
|
|
1787
1959
|
/**
|
|
1788
1960
|
* {{> grabHTMLFrom }}
|
|
1789
1961
|
*/
|
|
1790
1962
|
async grabHTMLFrom(locator) {
|
|
1791
|
-
const html = await this.grabHTMLFromAll(locator)
|
|
1792
|
-
assertElementExists(html, locator)
|
|
1963
|
+
const html = await this.grabHTMLFromAll(locator)
|
|
1964
|
+
assertElementExists(html, locator)
|
|
1793
1965
|
if (html.length > 1) {
|
|
1794
|
-
this.debugSection('GrabHTML', `Using first element out of ${html.length}`)
|
|
1966
|
+
this.debugSection('GrabHTML', `Using first element out of ${html.length}`)
|
|
1795
1967
|
}
|
|
1796
1968
|
|
|
1797
|
-
return html[0]
|
|
1969
|
+
return html[0]
|
|
1798
1970
|
}
|
|
1799
1971
|
|
|
1800
1972
|
/**
|
|
@@ -1802,11 +1974,11 @@ class Puppeteer extends Helper {
|
|
|
1802
1974
|
* {{ react }}
|
|
1803
1975
|
*/
|
|
1804
1976
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1805
|
-
const els = await this._locate(locator)
|
|
1806
|
-
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el)))
|
|
1807
|
-
const cssValues = res.map(props => props[toCamelCase(cssProperty)])
|
|
1977
|
+
const els = await this._locate(locator)
|
|
1978
|
+
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))))))
|
|
1979
|
+
const cssValues = res.map(props => props[toCamelCase(cssProperty)])
|
|
1808
1980
|
|
|
1809
|
-
return cssValues
|
|
1981
|
+
return cssValues
|
|
1810
1982
|
}
|
|
1811
1983
|
|
|
1812
1984
|
/**
|
|
@@ -1814,14 +1986,14 @@ class Puppeteer extends Helper {
|
|
|
1814
1986
|
* {{ react }}
|
|
1815
1987
|
*/
|
|
1816
1988
|
async grabCssPropertyFrom(locator, cssProperty) {
|
|
1817
|
-
const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
|
|
1818
|
-
assertElementExists(cssValues, locator)
|
|
1989
|
+
const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty)
|
|
1990
|
+
assertElementExists(cssValues, locator)
|
|
1819
1991
|
|
|
1820
1992
|
if (cssValues.length > 1) {
|
|
1821
|
-
this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`)
|
|
1993
|
+
this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`)
|
|
1822
1994
|
}
|
|
1823
1995
|
|
|
1824
|
-
return cssValues[0]
|
|
1996
|
+
return cssValues[0]
|
|
1825
1997
|
}
|
|
1826
1998
|
|
|
1827
1999
|
/**
|
|
@@ -1829,35 +2001,34 @@ class Puppeteer extends Helper {
|
|
|
1829
2001
|
* {{ react }}
|
|
1830
2002
|
*/
|
|
1831
2003
|
async seeCssPropertiesOnElements(locator, cssProperties) {
|
|
1832
|
-
const res = await this._locate(locator)
|
|
1833
|
-
assertElementExists(res, locator)
|
|
2004
|
+
const res = await this._locate(locator)
|
|
2005
|
+
assertElementExists(res, locator)
|
|
1834
2006
|
|
|
1835
|
-
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties)
|
|
1836
|
-
const elemAmount = res.length
|
|
1837
|
-
let props = []
|
|
2007
|
+
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties)
|
|
2008
|
+
const elemAmount = res.length
|
|
2009
|
+
let props = []
|
|
1838
2010
|
|
|
1839
2011
|
for (const element of res) {
|
|
1840
2012
|
for (const prop of Object.keys(cssProperties)) {
|
|
1841
|
-
const cssProp = await this.grabCssPropertyFrom(locator, prop)
|
|
2013
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop)
|
|
1842
2014
|
if (isColorProperty(prop)) {
|
|
1843
|
-
props.push(convertColorToRGBA(cssProp))
|
|
2015
|
+
props.push(convertColorToRGBA(cssProp))
|
|
1844
2016
|
} else {
|
|
1845
|
-
props.push(cssProp)
|
|
2017
|
+
props.push(cssProp)
|
|
1846
2018
|
}
|
|
1847
2019
|
}
|
|
1848
2020
|
}
|
|
1849
2021
|
|
|
1850
|
-
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
|
|
1851
|
-
if (!Array.isArray(props)) props = [props]
|
|
1852
|
-
let chunked = chunkArray(props, values.length)
|
|
1853
|
-
chunked = chunked.filter(
|
|
2022
|
+
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key])
|
|
2023
|
+
if (!Array.isArray(props)) props = [props]
|
|
2024
|
+
let chunked = chunkArray(props, values.length)
|
|
2025
|
+
chunked = chunked.filter(val => {
|
|
1854
2026
|
for (let i = 0; i < val.length; ++i) {
|
|
1855
|
-
|
|
1856
|
-
if (val[i] != values[i]) return false;
|
|
2027
|
+
if (val[i] != values[i]) return false
|
|
1857
2028
|
}
|
|
1858
|
-
return true
|
|
1859
|
-
})
|
|
1860
|
-
return equals(`all elements (${
|
|
2029
|
+
return true
|
|
2030
|
+
})
|
|
2031
|
+
return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount)
|
|
1861
2032
|
}
|
|
1862
2033
|
|
|
1863
2034
|
/**
|
|
@@ -1865,34 +2036,37 @@ class Puppeteer extends Helper {
|
|
|
1865
2036
|
* {{ react }}
|
|
1866
2037
|
*/
|
|
1867
2038
|
async seeAttributesOnElements(locator, attributes) {
|
|
1868
|
-
const elements = await this._locate(locator)
|
|
1869
|
-
assertElementExists(elements, locator)
|
|
2039
|
+
const elements = await this._locate(locator)
|
|
2040
|
+
assertElementExists(elements, locator)
|
|
1870
2041
|
|
|
1871
|
-
const expectedAttributes = Object.entries(attributes)
|
|
2042
|
+
const expectedAttributes = Object.entries(attributes)
|
|
1872
2043
|
|
|
1873
|
-
const valuesPromises = elements.map(async
|
|
1874
|
-
const elementAttributes = {}
|
|
1875
|
-
await Promise.all(
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2044
|
+
const valuesPromises = elements.map(async element => {
|
|
2045
|
+
const elementAttributes = {}
|
|
2046
|
+
await Promise.all(
|
|
2047
|
+
expectedAttributes.map(async ([attribute, expectedValue]) => {
|
|
2048
|
+
const actualValue = await element.evaluate((el, attr) => el[attr] || el.getAttribute(attr), attribute)
|
|
2049
|
+
elementAttributes[attribute] = actualValue
|
|
2050
|
+
}),
|
|
2051
|
+
)
|
|
2052
|
+
return elementAttributes
|
|
2053
|
+
})
|
|
1881
2054
|
|
|
1882
|
-
const actualAttributes = await Promise.all(valuesPromises)
|
|
2055
|
+
const actualAttributes = await Promise.all(valuesPromises)
|
|
1883
2056
|
|
|
1884
|
-
const matchingElements = actualAttributes.filter(
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2057
|
+
const matchingElements = actualAttributes.filter(attrs =>
|
|
2058
|
+
expectedAttributes.every(([attribute, expectedValue]) => {
|
|
2059
|
+
const actualValue = attrs[attribute]
|
|
2060
|
+
if (!actualValue) return false
|
|
2061
|
+
if (actualValue.toString().match(new RegExp(expectedValue.toString()))) return true
|
|
2062
|
+
return expectedValue === actualValue
|
|
2063
|
+
}),
|
|
2064
|
+
)
|
|
1890
2065
|
|
|
1891
|
-
const elementsCount = elements.length
|
|
1892
|
-
const matchingCount = matchingElements.length
|
|
2066
|
+
const elementsCount = elements.length
|
|
2067
|
+
const matchingCount = matchingElements.length
|
|
1893
2068
|
|
|
1894
|
-
return equals(`all elements (${
|
|
1895
|
-
.assert(matchingCount, elementsCount);
|
|
2069
|
+
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(matchingCount, elementsCount)
|
|
1896
2070
|
}
|
|
1897
2071
|
|
|
1898
2072
|
/**
|
|
@@ -1900,21 +2074,21 @@ class Puppeteer extends Helper {
|
|
|
1900
2074
|
* {{ react }}
|
|
1901
2075
|
*/
|
|
1902
2076
|
async dragSlider(locator, offsetX = 0) {
|
|
1903
|
-
const src = await this._locate(locator)
|
|
1904
|
-
assertElementExists(src, locator, 'Slider Element')
|
|
2077
|
+
const src = await this._locate(locator)
|
|
2078
|
+
assertElementExists(src, locator, 'Slider Element')
|
|
1905
2079
|
|
|
1906
2080
|
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
|
|
1907
|
-
const sliderSource = await getClickablePoint(src[0])
|
|
2081
|
+
const sliderSource = await getClickablePoint(src[0])
|
|
1908
2082
|
|
|
1909
2083
|
// Drag start point
|
|
1910
|
-
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 })
|
|
1911
|
-
await this.page.mouse.down()
|
|
2084
|
+
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 })
|
|
2085
|
+
await this.page.mouse.down()
|
|
1912
2086
|
|
|
1913
2087
|
// Drag destination
|
|
1914
|
-
await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 })
|
|
1915
|
-
await this.page.mouse.up()
|
|
2088
|
+
await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 })
|
|
2089
|
+
await this.page.mouse.up()
|
|
1916
2090
|
|
|
1917
|
-
await this._waitForAction()
|
|
2091
|
+
await this._waitForAction()
|
|
1918
2092
|
}
|
|
1919
2093
|
|
|
1920
2094
|
/**
|
|
@@ -1922,13 +2096,13 @@ class Puppeteer extends Helper {
|
|
|
1922
2096
|
* {{ react }}
|
|
1923
2097
|
*/
|
|
1924
2098
|
async grabAttributeFromAll(locator, attr) {
|
|
1925
|
-
const els = await this._locate(locator)
|
|
1926
|
-
const array = []
|
|
2099
|
+
const els = await this._locate(locator)
|
|
2100
|
+
const array = []
|
|
1927
2101
|
for (let index = 0; index < els.length; index++) {
|
|
1928
|
-
const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
|
|
1929
|
-
array.push(
|
|
2102
|
+
const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
|
|
2103
|
+
array.push(a)
|
|
1930
2104
|
}
|
|
1931
|
-
return array
|
|
2105
|
+
return array
|
|
1932
2106
|
}
|
|
1933
2107
|
|
|
1934
2108
|
/**
|
|
@@ -1936,84 +2110,90 @@ class Puppeteer extends Helper {
|
|
|
1936
2110
|
* {{ react }}
|
|
1937
2111
|
*/
|
|
1938
2112
|
async grabAttributeFrom(locator, attr) {
|
|
1939
|
-
const attrs = await this.grabAttributeFromAll(locator, attr)
|
|
1940
|
-
assertElementExists(attrs, locator)
|
|
2113
|
+
const attrs = await this.grabAttributeFromAll(locator, attr)
|
|
2114
|
+
assertElementExists(attrs, locator)
|
|
1941
2115
|
if (attrs.length > 1) {
|
|
1942
|
-
this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`)
|
|
2116
|
+
this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`)
|
|
1943
2117
|
}
|
|
1944
2118
|
|
|
1945
|
-
return attrs[0]
|
|
2119
|
+
return attrs[0]
|
|
1946
2120
|
}
|
|
1947
2121
|
|
|
1948
2122
|
/**
|
|
1949
2123
|
* {{> saveElementScreenshot }}
|
|
1950
2124
|
*/
|
|
1951
2125
|
async saveElementScreenshot(locator, fileName) {
|
|
1952
|
-
const outputFile = screenshotOutputFolder(fileName)
|
|
2126
|
+
const outputFile = screenshotOutputFolder(fileName)
|
|
1953
2127
|
|
|
1954
|
-
const res = await this._locate(locator)
|
|
1955
|
-
assertElementExists(res, locator)
|
|
1956
|
-
if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`)
|
|
1957
|
-
const elem = res[0]
|
|
1958
|
-
this.debug(`Screenshot of ${
|
|
1959
|
-
return elem.screenshot({ path: outputFile, type: 'png' })
|
|
2128
|
+
const res = await this._locate(locator)
|
|
2129
|
+
assertElementExists(res, locator)
|
|
2130
|
+
if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`)
|
|
2131
|
+
const elem = res[0]
|
|
2132
|
+
this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
|
|
2133
|
+
return elem.screenshot({ path: outputFile, type: 'png' })
|
|
1960
2134
|
}
|
|
1961
2135
|
|
|
1962
2136
|
/**
|
|
1963
2137
|
* {{> saveScreenshot }}
|
|
1964
2138
|
*/
|
|
1965
2139
|
async saveScreenshot(fileName, fullPage) {
|
|
1966
|
-
const fullPageOption = fullPage || this.options.fullPageScreenshots
|
|
1967
|
-
let outputFile = screenshotOutputFolder(fileName)
|
|
2140
|
+
const fullPageOption = fullPage || this.options.fullPageScreenshots
|
|
2141
|
+
let outputFile = screenshotOutputFolder(fileName)
|
|
2142
|
+
|
|
2143
|
+
this.debug(`Screenshot is saving to ${outputFile}`)
|
|
1968
2144
|
|
|
1969
|
-
|
|
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
|
+
}
|
|
1970
2150
|
|
|
1971
2151
|
await this.page.screenshot({
|
|
1972
2152
|
path: outputFile,
|
|
1973
2153
|
fullPage: fullPageOption,
|
|
1974
2154
|
type: 'png',
|
|
1975
|
-
})
|
|
2155
|
+
})
|
|
1976
2156
|
|
|
1977
2157
|
if (this.activeSessionName) {
|
|
1978
2158
|
for (const sessionName in this.sessionPages) {
|
|
1979
|
-
const activeSessionPage = this.sessionPages[sessionName]
|
|
1980
|
-
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
|
|
2159
|
+
const activeSessionPage = this.sessionPages[sessionName]
|
|
2160
|
+
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`)
|
|
1981
2161
|
|
|
1982
|
-
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
|
|
2162
|
+
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
|
|
1983
2163
|
|
|
1984
|
-
if (activeSessionPage) {
|
|
2164
|
+
if (activeSessionPage && !activeSessionPage.isClosed?.()) {
|
|
1985
2165
|
await activeSessionPage.screenshot({
|
|
1986
2166
|
path: outputFile,
|
|
1987
2167
|
fullPage: fullPageOption,
|
|
1988
2168
|
type: 'png',
|
|
1989
|
-
})
|
|
2169
|
+
})
|
|
1990
2170
|
}
|
|
1991
2171
|
}
|
|
1992
2172
|
}
|
|
1993
2173
|
}
|
|
1994
2174
|
|
|
1995
2175
|
async _failed(test) {
|
|
1996
|
-
await this._withinEnd()
|
|
2176
|
+
await this._withinEnd()
|
|
1997
2177
|
|
|
1998
2178
|
if (this.options.trace) {
|
|
1999
|
-
await this.page.tracing.stop()
|
|
2000
|
-
const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.failed.json')
|
|
2001
|
-
fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
|
|
2002
|
-
test.artifacts.trace = _traceName
|
|
2179
|
+
await this.page.tracing.stop()
|
|
2180
|
+
const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.failed.json')
|
|
2181
|
+
fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
|
|
2182
|
+
test.artifacts.trace = _traceName
|
|
2003
2183
|
}
|
|
2004
2184
|
}
|
|
2005
2185
|
|
|
2006
2186
|
async _passed(test) {
|
|
2007
|
-
await this._withinEnd()
|
|
2187
|
+
await this._withinEnd()
|
|
2008
2188
|
|
|
2009
2189
|
if (this.options.trace) {
|
|
2010
|
-
await this.page.tracing.stop()
|
|
2190
|
+
await this.page.tracing.stop()
|
|
2011
2191
|
if (this.options.keepTraceForPassedTests) {
|
|
2012
|
-
const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.passed.json')
|
|
2013
|
-
fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
|
|
2014
|
-
test.artifacts.trace = _traceName
|
|
2192
|
+
const _traceName = this.currentRunningTest.artifacts.trace.replace('.json', '.passed.json')
|
|
2193
|
+
fs.renameSync(this.currentRunningTest.artifacts.trace, _traceName)
|
|
2194
|
+
test.artifacts.trace = _traceName
|
|
2015
2195
|
} else {
|
|
2016
|
-
fs.unlinkSync(this.currentRunningTest.artifacts.trace)
|
|
2196
|
+
fs.unlinkSync(this.currentRunningTest.artifacts.trace)
|
|
2017
2197
|
}
|
|
2018
2198
|
}
|
|
2019
2199
|
}
|
|
@@ -2022,70 +2202,70 @@ class Puppeteer extends Helper {
|
|
|
2022
2202
|
* {{> wait }}
|
|
2023
2203
|
*/
|
|
2024
2204
|
async wait(sec) {
|
|
2025
|
-
return new Promise(
|
|
2026
|
-
setTimeout(done, sec * 1000)
|
|
2027
|
-
})
|
|
2205
|
+
return new Promise(done => {
|
|
2206
|
+
setTimeout(done, sec * 1000)
|
|
2207
|
+
})
|
|
2028
2208
|
}
|
|
2029
2209
|
|
|
2030
2210
|
/**
|
|
2031
2211
|
* {{> waitForEnabled }}
|
|
2032
2212
|
*/
|
|
2033
2213
|
async waitForEnabled(locator, sec) {
|
|
2034
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2035
|
-
locator = new Locator(locator, 'css')
|
|
2036
|
-
await this.context
|
|
2037
|
-
let waiter
|
|
2038
|
-
const context = await this._getContext()
|
|
2214
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2215
|
+
locator = new Locator(locator, 'css')
|
|
2216
|
+
await this.context
|
|
2217
|
+
let waiter
|
|
2218
|
+
const context = await this._getContext()
|
|
2039
2219
|
if (locator.isCSS()) {
|
|
2040
2220
|
const enabledFn = function (locator) {
|
|
2041
|
-
const els = document.querySelectorAll(locator)
|
|
2221
|
+
const els = document.querySelectorAll(locator)
|
|
2042
2222
|
if (!els || els.length === 0) {
|
|
2043
|
-
return false
|
|
2223
|
+
return false
|
|
2044
2224
|
}
|
|
2045
|
-
return Array.prototype.filter.call(els, el => !el.disabled).length > 0
|
|
2046
|
-
}
|
|
2047
|
-
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value)
|
|
2225
|
+
return Array.prototype.filter.call(els, el => !el.disabled).length > 0
|
|
2226
|
+
}
|
|
2227
|
+
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value)
|
|
2048
2228
|
} else {
|
|
2049
2229
|
const enabledFn = function (locator, $XPath) {
|
|
2050
|
-
eval($XPath)
|
|
2051
|
-
return $XPath(null, locator).filter(el => !el.disabled).length > 0
|
|
2052
|
-
}
|
|
2053
|
-
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2230
|
+
eval($XPath)
|
|
2231
|
+
return $XPath(null, locator).filter(el => !el.disabled).length > 0
|
|
2232
|
+
}
|
|
2233
|
+
waiter = context.waitForFunction(enabledFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2054
2234
|
}
|
|
2055
|
-
return waiter.catch(
|
|
2056
|
-
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2057
|
-
})
|
|
2235
|
+
return waiter.catch(err => {
|
|
2236
|
+
throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2237
|
+
})
|
|
2058
2238
|
}
|
|
2059
2239
|
|
|
2060
2240
|
/**
|
|
2061
2241
|
* {{> waitForValue }}
|
|
2062
2242
|
*/
|
|
2063
2243
|
async waitForValue(field, value, sec) {
|
|
2064
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2065
|
-
const locator = new Locator(field, 'css')
|
|
2066
|
-
await this.context
|
|
2067
|
-
let waiter
|
|
2068
|
-
const context = await this._getContext()
|
|
2244
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2245
|
+
const locator = new Locator(field, 'css')
|
|
2246
|
+
await this.context
|
|
2247
|
+
let waiter
|
|
2248
|
+
const context = await this._getContext()
|
|
2069
2249
|
if (locator.isCSS()) {
|
|
2070
2250
|
const valueFn = function (locator, value) {
|
|
2071
|
-
const els = document.querySelectorAll(locator)
|
|
2251
|
+
const els = document.querySelectorAll(locator)
|
|
2072
2252
|
if (!els || els.length === 0) {
|
|
2073
|
-
return false
|
|
2253
|
+
return false
|
|
2074
2254
|
}
|
|
2075
|
-
return Array.prototype.filter.call(els, el => (el.value.toString() || '').indexOf(value) !== -1).length > 0
|
|
2076
|
-
}
|
|
2077
|
-
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value)
|
|
2255
|
+
return Array.prototype.filter.call(els, el => (el.value.toString() || '').indexOf(value) !== -1).length > 0
|
|
2256
|
+
}
|
|
2257
|
+
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, value)
|
|
2078
2258
|
} else {
|
|
2079
2259
|
const valueFn = function (locator, $XPath, value) {
|
|
2080
|
-
eval($XPath)
|
|
2081
|
-
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2082
|
-
}
|
|
2083
|
-
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value)
|
|
2260
|
+
eval($XPath)
|
|
2261
|
+
return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0
|
|
2262
|
+
}
|
|
2263
|
+
waiter = context.waitForFunction(valueFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), value)
|
|
2084
2264
|
}
|
|
2085
|
-
return waiter.catch(
|
|
2086
|
-
const loc = locator.toString()
|
|
2087
|
-
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}`)
|
|
2088
|
-
})
|
|
2265
|
+
return waiter.catch(err => {
|
|
2266
|
+
const loc = locator.toString()
|
|
2267
|
+
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}`)
|
|
2268
|
+
})
|
|
2089
2269
|
}
|
|
2090
2270
|
|
|
2091
2271
|
/**
|
|
@@ -2093,45 +2273,47 @@ class Puppeteer extends Helper {
|
|
|
2093
2273
|
* {{ react }}
|
|
2094
2274
|
*/
|
|
2095
2275
|
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
2096
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2097
|
-
locator = new Locator(locator, 'css')
|
|
2098
|
-
let waiter
|
|
2099
|
-
const context = await this._getContext()
|
|
2276
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2277
|
+
locator = new Locator(locator, 'css')
|
|
2278
|
+
let waiter
|
|
2279
|
+
const context = await this._getContext()
|
|
2100
2280
|
if (locator.isCSS()) {
|
|
2101
2281
|
const visibleFn = function (locator, num) {
|
|
2102
|
-
const els = document.querySelectorAll(locator)
|
|
2282
|
+
const els = document.querySelectorAll(locator)
|
|
2103
2283
|
if (!els || els.length === 0) {
|
|
2104
|
-
return false
|
|
2284
|
+
return false
|
|
2105
2285
|
}
|
|
2106
|
-
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
|
|
2107
|
-
}
|
|
2108
|
-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num)
|
|
2286
|
+
return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num
|
|
2287
|
+
}
|
|
2288
|
+
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, num)
|
|
2109
2289
|
} else {
|
|
2110
2290
|
const visibleFn = function (locator, $XPath, num) {
|
|
2111
|
-
eval($XPath)
|
|
2112
|
-
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
|
|
2113
|
-
}
|
|
2114
|
-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num)
|
|
2291
|
+
eval($XPath)
|
|
2292
|
+
return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num
|
|
2293
|
+
}
|
|
2294
|
+
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString(), num)
|
|
2115
2295
|
}
|
|
2116
|
-
return waiter.catch(
|
|
2117
|
-
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2118
|
-
})
|
|
2296
|
+
return waiter.catch(err => {
|
|
2297
|
+
throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2298
|
+
})
|
|
2119
2299
|
}
|
|
2120
2300
|
|
|
2121
2301
|
/**
|
|
2122
2302
|
* {{> waitForClickable }}
|
|
2123
2303
|
*/
|
|
2124
2304
|
async waitForClickable(locator, waitTimeout) {
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2305
|
+
const el = await this._locateElement(locator)
|
|
2306
|
+
if (!el) {
|
|
2307
|
+
throw new ElementNotFound(locator, 'Element to wait for clickable')
|
|
2308
|
+
}
|
|
2127
2309
|
|
|
2128
|
-
return this.waitForFunction(isElementClickable, [
|
|
2310
|
+
return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
|
|
2129
2311
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2130
|
-
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
|
|
2312
|
+
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
|
|
2131
2313
|
} else {
|
|
2132
|
-
throw e
|
|
2314
|
+
throw e
|
|
2133
2315
|
}
|
|
2134
|
-
})
|
|
2316
|
+
})
|
|
2135
2317
|
}
|
|
2136
2318
|
|
|
2137
2319
|
/**
|
|
@@ -2139,19 +2321,19 @@ class Puppeteer extends Helper {
|
|
|
2139
2321
|
* {{ react }}
|
|
2140
2322
|
*/
|
|
2141
2323
|
async waitForElement(locator, sec) {
|
|
2142
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2143
|
-
locator = new Locator(locator, 'css')
|
|
2324
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2325
|
+
locator = new Locator(locator, 'css')
|
|
2144
2326
|
|
|
2145
|
-
let waiter
|
|
2146
|
-
const context = await this._getContext()
|
|
2327
|
+
let waiter
|
|
2328
|
+
const context = await this._getContext()
|
|
2147
2329
|
if (locator.isCSS()) {
|
|
2148
|
-
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout })
|
|
2330
|
+
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout })
|
|
2149
2331
|
} else {
|
|
2150
|
-
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout })
|
|
2332
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout })
|
|
2151
2333
|
}
|
|
2152
|
-
return waiter.catch(
|
|
2153
|
-
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2154
|
-
})
|
|
2334
|
+
return waiter.catch(err => {
|
|
2335
|
+
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2336
|
+
})
|
|
2155
2337
|
}
|
|
2156
2338
|
|
|
2157
2339
|
/**
|
|
@@ -2160,160 +2342,183 @@ class Puppeteer extends Helper {
|
|
|
2160
2342
|
* {{ react }}
|
|
2161
2343
|
*/
|
|
2162
2344
|
async waitForVisible(locator, sec) {
|
|
2163
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2164
|
-
locator = new Locator(locator, 'css')
|
|
2165
|
-
await this.context
|
|
2166
|
-
let waiter
|
|
2167
|
-
const context = await this._getContext()
|
|
2345
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2346
|
+
locator = new Locator(locator, 'css')
|
|
2347
|
+
await this.context
|
|
2348
|
+
let waiter
|
|
2349
|
+
const context = await this._getContext()
|
|
2168
2350
|
if (locator.isCSS()) {
|
|
2169
|
-
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true })
|
|
2351
|
+
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true })
|
|
2170
2352
|
} else {
|
|
2171
|
-
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true })
|
|
2353
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true })
|
|
2172
2354
|
}
|
|
2173
|
-
return waiter.catch(
|
|
2174
|
-
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2175
|
-
})
|
|
2355
|
+
return waiter.catch(err => {
|
|
2356
|
+
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2357
|
+
})
|
|
2176
2358
|
}
|
|
2177
2359
|
|
|
2178
2360
|
/**
|
|
2179
2361
|
* {{> waitForInvisible }}
|
|
2180
2362
|
*/
|
|
2181
2363
|
async waitForInvisible(locator, sec) {
|
|
2182
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2183
|
-
locator = new Locator(locator, 'css')
|
|
2184
|
-
await this.context
|
|
2185
|
-
let waiter
|
|
2186
|
-
const context = await this._getContext()
|
|
2364
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2365
|
+
locator = new Locator(locator, 'css')
|
|
2366
|
+
await this.context
|
|
2367
|
+
let waiter
|
|
2368
|
+
const context = await this._getContext()
|
|
2187
2369
|
if (locator.isCSS()) {
|
|
2188
|
-
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
|
|
2370
|
+
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
|
|
2189
2371
|
} else {
|
|
2190
|
-
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2372
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2191
2373
|
}
|
|
2192
|
-
return waiter.catch(
|
|
2193
|
-
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2194
|
-
})
|
|
2374
|
+
return waiter.catch(err => {
|
|
2375
|
+
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2376
|
+
})
|
|
2195
2377
|
}
|
|
2196
2378
|
|
|
2197
2379
|
/**
|
|
2198
2380
|
* {{> waitToHide }}
|
|
2199
2381
|
*/
|
|
2200
2382
|
async waitToHide(locator, sec) {
|
|
2201
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2202
|
-
locator = new Locator(locator, 'css')
|
|
2203
|
-
let waiter
|
|
2204
|
-
const context = await this._getContext()
|
|
2383
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2384
|
+
locator = new Locator(locator, 'css')
|
|
2385
|
+
let waiter
|
|
2386
|
+
const context = await this._getContext()
|
|
2205
2387
|
if (locator.isCSS()) {
|
|
2206
|
-
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
|
|
2388
|
+
waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true })
|
|
2207
2389
|
} else {
|
|
2208
|
-
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2390
|
+
waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true })
|
|
2209
2391
|
}
|
|
2210
|
-
return waiter.catch(
|
|
2211
|
-
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2212
|
-
})
|
|
2392
|
+
return waiter.catch(err => {
|
|
2393
|
+
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2394
|
+
})
|
|
2213
2395
|
}
|
|
2214
2396
|
|
|
2215
2397
|
/**
|
|
2216
2398
|
* {{> waitForNumberOfTabs }}
|
|
2217
2399
|
*/
|
|
2218
2400
|
async waitForNumberOfTabs(expectedTabs, sec) {
|
|
2219
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2220
|
-
let currentTabs
|
|
2221
|
-
let count = 0
|
|
2401
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2402
|
+
let currentTabs
|
|
2403
|
+
let count = 0
|
|
2222
2404
|
|
|
2223
2405
|
do {
|
|
2224
|
-
currentTabs = await this.grabNumberOfOpenTabs()
|
|
2225
|
-
await this.wait(1)
|
|
2226
|
-
count += 1000
|
|
2227
|
-
if (currentTabs >= expectedTabs) return
|
|
2228
|
-
} while (count <= waitTimeout)
|
|
2406
|
+
currentTabs = await this.grabNumberOfOpenTabs()
|
|
2407
|
+
await this.wait(1)
|
|
2408
|
+
count += 1000
|
|
2409
|
+
if (currentTabs >= expectedTabs) return
|
|
2410
|
+
} while (count <= waitTimeout)
|
|
2229
2411
|
|
|
2230
|
-
throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`)
|
|
2412
|
+
throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`)
|
|
2231
2413
|
}
|
|
2232
2414
|
|
|
2233
2415
|
async _getContext() {
|
|
2234
2416
|
if (this.context && this.context.constructor.name === 'CdpFrame') {
|
|
2235
|
-
return this.context
|
|
2417
|
+
return this.context
|
|
2236
2418
|
}
|
|
2237
|
-
return this.page
|
|
2419
|
+
return this.page
|
|
2238
2420
|
}
|
|
2239
2421
|
|
|
2240
2422
|
/**
|
|
2241
2423
|
* {{> waitInUrl }}
|
|
2242
2424
|
*/
|
|
2243
2425
|
async waitInUrl(urlPart, sec = null) {
|
|
2244
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2245
|
-
|
|
2246
|
-
return this.page
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2426
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2427
|
+
|
|
2428
|
+
return this.page
|
|
2429
|
+
.waitForFunction(
|
|
2430
|
+
urlPart => {
|
|
2431
|
+
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2432
|
+
return currUrl.indexOf(urlPart) > -1
|
|
2433
|
+
},
|
|
2434
|
+
{ timeout: waitTimeout },
|
|
2435
|
+
urlPart,
|
|
2436
|
+
)
|
|
2437
|
+
.catch(async e => {
|
|
2438
|
+
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2439
|
+
if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2440
|
+
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
|
|
2441
|
+
} else {
|
|
2442
|
+
throw e
|
|
2443
|
+
}
|
|
2444
|
+
})
|
|
2257
2445
|
}
|
|
2258
2446
|
|
|
2259
2447
|
/**
|
|
2260
2448
|
* {{> waitUrlEquals }}
|
|
2261
2449
|
*/
|
|
2262
2450
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2263
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2451
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2264
2452
|
|
|
2265
|
-
const baseUrl = this.options.url
|
|
2453
|
+
const baseUrl = this.options.url
|
|
2266
2454
|
if (urlPart.indexOf('http') < 0) {
|
|
2267
|
-
urlPart = baseUrl + urlPart
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
return this.page
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2455
|
+
urlPart = baseUrl + urlPart
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
return this.page
|
|
2459
|
+
.waitForFunction(
|
|
2460
|
+
urlPart => {
|
|
2461
|
+
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
2462
|
+
return currUrl.indexOf(urlPart) > -1
|
|
2463
|
+
},
|
|
2464
|
+
{ timeout: waitTimeout },
|
|
2465
|
+
urlPart,
|
|
2466
|
+
)
|
|
2467
|
+
.catch(async e => {
|
|
2468
|
+
const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
|
|
2469
|
+
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2470
|
+
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
|
|
2471
|
+
} else {
|
|
2472
|
+
throw e
|
|
2473
|
+
}
|
|
2474
|
+
})
|
|
2281
2475
|
}
|
|
2282
2476
|
|
|
2283
2477
|
/**
|
|
2284
2478
|
* {{> waitForText }}
|
|
2285
2479
|
*/
|
|
2286
2480
|
async waitForText(text, sec = null, context = null) {
|
|
2287
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2288
|
-
let waiter
|
|
2481
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2482
|
+
let waiter
|
|
2289
2483
|
|
|
2290
|
-
const contextObject = await this._getContext()
|
|
2484
|
+
const contextObject = await this._getContext()
|
|
2291
2485
|
|
|
2292
2486
|
if (context) {
|
|
2293
|
-
const locator = new Locator(context, 'css')
|
|
2487
|
+
const locator = new Locator(context, 'css')
|
|
2294
2488
|
if (locator.isCSS()) {
|
|
2295
|
-
waiter = contextObject.waitForFunction(
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2489
|
+
waiter = contextObject.waitForFunction(
|
|
2490
|
+
(locator, text) => {
|
|
2491
|
+
const el = document.querySelector(locator)
|
|
2492
|
+
if (!el) return false
|
|
2493
|
+
return el.innerText.indexOf(text) > -1
|
|
2494
|
+
},
|
|
2495
|
+
{ timeout: waitTimeout },
|
|
2496
|
+
locator.value,
|
|
2497
|
+
text,
|
|
2498
|
+
)
|
|
2300
2499
|
}
|
|
2301
2500
|
|
|
2302
2501
|
if (locator.isXPath()) {
|
|
2303
|
-
waiter = contextObject.waitForFunction(
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2502
|
+
waiter = contextObject.waitForFunction(
|
|
2503
|
+
(locator, text, $XPath) => {
|
|
2504
|
+
eval($XPath)
|
|
2505
|
+
const el = $XPath(null, locator)
|
|
2506
|
+
if (!el.length) return false
|
|
2507
|
+
return el[0].innerText.indexOf(text) > -1
|
|
2508
|
+
},
|
|
2509
|
+
{ timeout: waitTimeout },
|
|
2510
|
+
locator.value,
|
|
2511
|
+
text,
|
|
2512
|
+
$XPath.toString(),
|
|
2513
|
+
)
|
|
2309
2514
|
}
|
|
2310
2515
|
} else {
|
|
2311
|
-
waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text)
|
|
2516
|
+
waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, { timeout: waitTimeout }, text)
|
|
2312
2517
|
}
|
|
2313
2518
|
|
|
2314
|
-
return waiter.catch(
|
|
2315
|
-
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2316
|
-
})
|
|
2519
|
+
return waiter.catch(err => {
|
|
2520
|
+
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2521
|
+
})
|
|
2317
2522
|
}
|
|
2318
2523
|
|
|
2319
2524
|
/**
|
|
@@ -2328,8 +2533,8 @@ class Puppeteer extends Helper {
|
|
|
2328
2533
|
* @param {?number} [sec=null] seconds to wait
|
|
2329
2534
|
*/
|
|
2330
2535
|
async waitForRequest(urlOrPredicate, sec = null) {
|
|
2331
|
-
const timeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2332
|
-
return this.page.waitForRequest(urlOrPredicate, { timeout })
|
|
2536
|
+
const timeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2537
|
+
return this.page.waitForRequest(urlOrPredicate, { timeout })
|
|
2333
2538
|
}
|
|
2334
2539
|
|
|
2335
2540
|
/**
|
|
@@ -2344,8 +2549,8 @@ class Puppeteer extends Helper {
|
|
|
2344
2549
|
* @param {?number} [sec=null] number of seconds to wait
|
|
2345
2550
|
*/
|
|
2346
2551
|
async waitForResponse(urlOrPredicate, sec = null) {
|
|
2347
|
-
const timeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2348
|
-
return this.page.waitForResponse(urlOrPredicate, { timeout })
|
|
2552
|
+
const timeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2553
|
+
return this.page.waitForResponse(urlOrPredicate, { timeout })
|
|
2349
2554
|
}
|
|
2350
2555
|
|
|
2351
2556
|
/**
|
|
@@ -2354,37 +2559,37 @@ class Puppeteer extends Helper {
|
|
|
2354
2559
|
async switchTo(locator) {
|
|
2355
2560
|
if (Number.isInteger(locator)) {
|
|
2356
2561
|
// Select by frame index of current context
|
|
2357
|
-
let frames = []
|
|
2562
|
+
let frames = []
|
|
2358
2563
|
if (this.context && typeof this.context.childFrames === 'function') {
|
|
2359
|
-
frames = await this.context.childFrames()
|
|
2564
|
+
frames = await this.context.childFrames()
|
|
2360
2565
|
} else {
|
|
2361
|
-
frames = await this.page.mainFrame().childFrames()
|
|
2566
|
+
frames = await this.page.mainFrame().childFrames()
|
|
2362
2567
|
}
|
|
2363
2568
|
|
|
2364
2569
|
if (locator >= 0 && locator < frames.length) {
|
|
2365
|
-
this.context = frames[locator]
|
|
2570
|
+
this.context = frames[locator]
|
|
2366
2571
|
} else {
|
|
2367
|
-
throw new Error('Frame index out of bounds')
|
|
2572
|
+
throw new Error('Frame index out of bounds')
|
|
2368
2573
|
}
|
|
2369
|
-
return
|
|
2574
|
+
return
|
|
2370
2575
|
}
|
|
2371
2576
|
|
|
2372
2577
|
if (!locator) {
|
|
2373
|
-
this.context = await this.page.mainFrame()
|
|
2374
|
-
return
|
|
2578
|
+
this.context = await this.page.mainFrame()
|
|
2579
|
+
return
|
|
2375
2580
|
}
|
|
2376
2581
|
|
|
2377
2582
|
// Select iframe by selector
|
|
2378
|
-
const els = await this._locate(locator)
|
|
2379
|
-
assertElementExists(els, locator)
|
|
2583
|
+
const els = await this._locate(locator)
|
|
2584
|
+
assertElementExists(els, locator)
|
|
2380
2585
|
|
|
2381
|
-
const iframeElement = els[0]
|
|
2382
|
-
const contentFrame = await iframeElement.contentFrame()
|
|
2586
|
+
const iframeElement = els[0]
|
|
2587
|
+
const contentFrame = await iframeElement.contentFrame()
|
|
2383
2588
|
|
|
2384
2589
|
if (contentFrame) {
|
|
2385
|
-
this.context = contentFrame
|
|
2590
|
+
this.context = contentFrame
|
|
2386
2591
|
} else {
|
|
2387
|
-
throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath')
|
|
2592
|
+
throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath')
|
|
2388
2593
|
}
|
|
2389
2594
|
}
|
|
2390
2595
|
|
|
@@ -2392,23 +2597,23 @@ class Puppeteer extends Helper {
|
|
|
2392
2597
|
* {{> waitForFunction }}
|
|
2393
2598
|
*/
|
|
2394
2599
|
async waitForFunction(fn, argsOrSec = null, sec = null) {
|
|
2395
|
-
let args = []
|
|
2600
|
+
let args = []
|
|
2396
2601
|
if (argsOrSec) {
|
|
2397
2602
|
if (Array.isArray(argsOrSec)) {
|
|
2398
|
-
args = argsOrSec
|
|
2603
|
+
args = argsOrSec
|
|
2399
2604
|
} else if (typeof argsOrSec === 'number') {
|
|
2400
|
-
sec = argsOrSec
|
|
2605
|
+
sec = argsOrSec
|
|
2401
2606
|
}
|
|
2402
2607
|
}
|
|
2403
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2404
|
-
const context = await this._getContext()
|
|
2405
|
-
return context.waitForFunction(fn, { timeout: waitTimeout }, ...args)
|
|
2608
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2609
|
+
const context = await this._getContext()
|
|
2610
|
+
return context.waitForFunction(fn, { timeout: waitTimeout }, ...args)
|
|
2406
2611
|
}
|
|
2407
2612
|
|
|
2408
2613
|
/**
|
|
2409
2614
|
* Waits for navigation to finish. By default, takes configured `waitForNavigation` option.
|
|
2410
2615
|
*
|
|
2411
|
-
* See [Puppeteer's reference](https://github.com/
|
|
2616
|
+
* See [Puppeteer's reference](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.page.waitfornavigation.md)
|
|
2412
2617
|
*
|
|
2413
2618
|
* @param {*} opts
|
|
2414
2619
|
*/
|
|
@@ -2417,87 +2622,87 @@ class Puppeteer extends Helper {
|
|
|
2417
2622
|
timeout: this.options.getPageTimeout,
|
|
2418
2623
|
waitUntil: this.options.waitForNavigation,
|
|
2419
2624
|
...opts,
|
|
2420
|
-
}
|
|
2421
|
-
return this.page.waitForNavigation(opts)
|
|
2625
|
+
}
|
|
2626
|
+
return this.page.waitForNavigation(opts)
|
|
2422
2627
|
}
|
|
2423
2628
|
|
|
2424
2629
|
async waitUntilExists(locator, sec) {
|
|
2425
2630
|
console.log(`waitUntilExists deprecated:
|
|
2426
2631
|
* use 'waitForElement' to wait for element to be attached
|
|
2427
|
-
* use 'waitForDetached to wait for element to be removed'`)
|
|
2428
|
-
return this.waitForDetached(locator, sec)
|
|
2632
|
+
* use 'waitForDetached to wait for element to be removed'`)
|
|
2633
|
+
return this.waitForDetached(locator, sec)
|
|
2429
2634
|
}
|
|
2430
2635
|
|
|
2431
2636
|
/**
|
|
2432
2637
|
* {{> waitForDetached }}
|
|
2433
2638
|
*/
|
|
2434
2639
|
async waitForDetached(locator, sec) {
|
|
2435
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2436
|
-
locator = new Locator(locator, 'css')
|
|
2640
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2641
|
+
locator = new Locator(locator, 'css')
|
|
2437
2642
|
|
|
2438
|
-
let waiter
|
|
2439
|
-
const context = await this._getContext()
|
|
2643
|
+
let waiter
|
|
2644
|
+
const context = await this._getContext()
|
|
2440
2645
|
if (locator.isCSS()) {
|
|
2441
2646
|
const visibleFn = function (locator) {
|
|
2442
|
-
return document.querySelector(locator) === null
|
|
2443
|
-
}
|
|
2444
|
-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value)
|
|
2647
|
+
return document.querySelector(locator) === null
|
|
2648
|
+
}
|
|
2649
|
+
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value)
|
|
2445
2650
|
} else {
|
|
2446
2651
|
const visibleFn = function (locator, $XPath) {
|
|
2447
|
-
eval($XPath)
|
|
2448
|
-
return $XPath(null, locator).length === 0
|
|
2449
|
-
}
|
|
2450
|
-
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2652
|
+
eval($XPath)
|
|
2653
|
+
return $XPath(null, locator).length === 0
|
|
2654
|
+
}
|
|
2655
|
+
waiter = context.waitForFunction(visibleFn, { timeout: waitTimeout }, locator.value, $XPath.toString())
|
|
2451
2656
|
}
|
|
2452
|
-
return waiter.catch(
|
|
2453
|
-
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2454
|
-
})
|
|
2657
|
+
return waiter.catch(err => {
|
|
2658
|
+
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`)
|
|
2659
|
+
})
|
|
2455
2660
|
}
|
|
2456
2661
|
|
|
2457
2662
|
async _waitForAction() {
|
|
2458
|
-
return this.wait(this.options.waitForAction / 1000)
|
|
2663
|
+
return this.wait(this.options.waitForAction / 1000)
|
|
2459
2664
|
}
|
|
2460
2665
|
|
|
2461
2666
|
/**
|
|
2462
2667
|
* {{> grabDataFromPerformanceTiming }}
|
|
2463
2668
|
*/
|
|
2464
2669
|
async grabDataFromPerformanceTiming() {
|
|
2465
|
-
return perfTiming
|
|
2670
|
+
return perfTiming
|
|
2466
2671
|
}
|
|
2467
2672
|
|
|
2468
2673
|
/**
|
|
2469
2674
|
* {{> grabElementBoundingRect }}
|
|
2470
2675
|
*/
|
|
2471
2676
|
async grabElementBoundingRect(locator, prop) {
|
|
2472
|
-
const els = await this._locate(locator)
|
|
2473
|
-
assertElementExists(els, locator)
|
|
2474
|
-
const rect = await els[0].boundingBox()
|
|
2475
|
-
if (prop) return rect[prop]
|
|
2476
|
-
return rect
|
|
2677
|
+
const els = await this._locate(locator)
|
|
2678
|
+
assertElementExists(els, locator)
|
|
2679
|
+
const rect = await els[0].boundingBox()
|
|
2680
|
+
if (prop) return rect[prop]
|
|
2681
|
+
return rect
|
|
2477
2682
|
}
|
|
2478
2683
|
|
|
2479
2684
|
/**
|
|
2480
|
-
* Mocks network request using [`Request Interception`](https://pptr.dev/
|
|
2685
|
+
* Mocks network request using [`Request Interception`](https://pptr.dev/guides/network-interception)
|
|
2481
2686
|
*
|
|
2482
2687
|
* ```js
|
|
2483
2688
|
* I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
|
|
2484
2689
|
* ```
|
|
2485
|
-
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/
|
|
2690
|
+
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/guides/network-interception)
|
|
2486
2691
|
*
|
|
2487
2692
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2488
2693
|
* @param {function} [handler] a function to process request
|
|
2489
2694
|
*/
|
|
2490
2695
|
async mockRoute(url, handler) {
|
|
2491
|
-
await this.page.setRequestInterception(true)
|
|
2696
|
+
await this.page.setRequestInterception(true)
|
|
2492
2697
|
|
|
2493
2698
|
this.page.on('request', interceptedRequest => {
|
|
2494
2699
|
if (interceptedRequest.url().match(url)) {
|
|
2495
2700
|
// @ts-ignore
|
|
2496
|
-
handler(interceptedRequest)
|
|
2701
|
+
handler(interceptedRequest)
|
|
2497
2702
|
} else {
|
|
2498
|
-
interceptedRequest.continue()
|
|
2703
|
+
interceptedRequest.continue()
|
|
2499
2704
|
}
|
|
2500
|
-
})
|
|
2705
|
+
})
|
|
2501
2706
|
}
|
|
2502
2707
|
|
|
2503
2708
|
/**
|
|
@@ -2510,18 +2715,18 @@ class Puppeteer extends Helper {
|
|
|
2510
2715
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2511
2716
|
*/
|
|
2512
2717
|
async stopMockingRoute(url) {
|
|
2513
|
-
await this.page.setRequestInterception(true)
|
|
2718
|
+
await this.page.setRequestInterception(true)
|
|
2514
2719
|
|
|
2515
|
-
this.page.off('request')
|
|
2720
|
+
this.page.off('request')
|
|
2516
2721
|
|
|
2517
2722
|
// Resume normal request handling for the given URL
|
|
2518
2723
|
this.page.on('request', interceptedRequest => {
|
|
2519
2724
|
if (interceptedRequest.url().includes(url)) {
|
|
2520
|
-
interceptedRequest.continue()
|
|
2725
|
+
interceptedRequest.continue()
|
|
2521
2726
|
} else {
|
|
2522
|
-
interceptedRequest.continue()
|
|
2727
|
+
interceptedRequest.continue()
|
|
2523
2728
|
}
|
|
2524
|
-
})
|
|
2729
|
+
})
|
|
2525
2730
|
}
|
|
2526
2731
|
|
|
2527
2732
|
/**
|
|
@@ -2529,15 +2734,16 @@ class Puppeteer extends Helper {
|
|
|
2529
2734
|
* {{> flushNetworkTraffics }}
|
|
2530
2735
|
*/
|
|
2531
2736
|
flushNetworkTraffics() {
|
|
2532
|
-
flushNetworkTraffics.call(this)
|
|
2737
|
+
flushNetworkTraffics.call(this)
|
|
2533
2738
|
}
|
|
2534
2739
|
|
|
2535
2740
|
/**
|
|
2536
2741
|
*
|
|
2537
2742
|
* {{> stopRecordingTraffic }}
|
|
2538
2743
|
*/
|
|
2539
|
-
stopRecordingTraffic() {
|
|
2540
|
-
|
|
2744
|
+
async stopRecordingTraffic() {
|
|
2745
|
+
await this.page.setRequestInterception(false)
|
|
2746
|
+
stopRecordingTraffic.call(this)
|
|
2541
2747
|
}
|
|
2542
2748
|
|
|
2543
2749
|
/**
|
|
@@ -2545,29 +2751,29 @@ class Puppeteer extends Helper {
|
|
|
2545
2751
|
*
|
|
2546
2752
|
*/
|
|
2547
2753
|
async startRecordingTraffic() {
|
|
2548
|
-
this.flushNetworkTraffics()
|
|
2549
|
-
this.recording = true
|
|
2550
|
-
this.recordedAtLeastOnce = true
|
|
2754
|
+
this.flushNetworkTraffics()
|
|
2755
|
+
this.recording = true
|
|
2756
|
+
this.recordedAtLeastOnce = true
|
|
2551
2757
|
|
|
2552
|
-
await this.page.setRequestInterception(true)
|
|
2758
|
+
await this.page.setRequestInterception(true)
|
|
2553
2759
|
|
|
2554
|
-
this.page.on('request',
|
|
2760
|
+
this.page.on('request', request => {
|
|
2555
2761
|
const information = {
|
|
2556
2762
|
url: request.url(),
|
|
2557
2763
|
method: request.method(),
|
|
2558
2764
|
requestHeaders: request.headers(),
|
|
2559
2765
|
requestPostData: request.postData(),
|
|
2560
2766
|
response: request.response(),
|
|
2561
|
-
}
|
|
2767
|
+
}
|
|
2562
2768
|
|
|
2563
|
-
this.debugSection('REQUEST: ', JSON.stringify(information))
|
|
2769
|
+
this.debugSection('REQUEST: ', JSON.stringify(information))
|
|
2564
2770
|
|
|
2565
2771
|
if (typeof information.requestPostData === 'object') {
|
|
2566
|
-
information.requestPostData = JSON.parse(information.requestPostData)
|
|
2772
|
+
information.requestPostData = JSON.parse(information.requestPostData)
|
|
2567
2773
|
}
|
|
2568
|
-
request.continue()
|
|
2569
|
-
this.requests.push(information)
|
|
2570
|
-
})
|
|
2774
|
+
request.continue()
|
|
2775
|
+
this.requests.push(information)
|
|
2776
|
+
})
|
|
2571
2777
|
}
|
|
2572
2778
|
|
|
2573
2779
|
/**
|
|
@@ -2575,17 +2781,15 @@ class Puppeteer extends Helper {
|
|
|
2575
2781
|
* {{> grabRecordedNetworkTraffics }}
|
|
2576
2782
|
*/
|
|
2577
2783
|
async grabRecordedNetworkTraffics() {
|
|
2578
|
-
return grabRecordedNetworkTraffics.call(this)
|
|
2784
|
+
return grabRecordedNetworkTraffics.call(this)
|
|
2579
2785
|
}
|
|
2580
2786
|
|
|
2581
2787
|
/**
|
|
2582
2788
|
*
|
|
2583
2789
|
* {{> seeTraffic }}
|
|
2584
2790
|
*/
|
|
2585
|
-
async seeTraffic({
|
|
2586
|
-
|
|
2587
|
-
}) {
|
|
2588
|
-
await seeTraffic.call(this, ...arguments);
|
|
2791
|
+
async seeTraffic({ name, url, parameters, requestPostData, timeout = 10 }) {
|
|
2792
|
+
await seeTraffic.call(this, ...arguments)
|
|
2589
2793
|
}
|
|
2590
2794
|
|
|
2591
2795
|
/**
|
|
@@ -2594,56 +2798,47 @@ class Puppeteer extends Helper {
|
|
|
2594
2798
|
*
|
|
2595
2799
|
*/
|
|
2596
2800
|
dontSeeTraffic({ name, url }) {
|
|
2597
|
-
dontSeeTraffic.call(this, ...arguments)
|
|
2801
|
+
dontSeeTraffic.call(this, ...arguments)
|
|
2598
2802
|
}
|
|
2599
2803
|
|
|
2600
2804
|
async getNewCDPSession() {
|
|
2601
|
-
const client = await this.page.target().createCDPSession()
|
|
2602
|
-
return client
|
|
2805
|
+
const client = await this.page.target().createCDPSession()
|
|
2806
|
+
return client
|
|
2603
2807
|
}
|
|
2604
2808
|
|
|
2605
2809
|
/**
|
|
2606
2810
|
* {{> startRecordingWebSocketMessages }}
|
|
2607
2811
|
*/
|
|
2608
2812
|
async startRecordingWebSocketMessages() {
|
|
2609
|
-
this.flushWebSocketMessages()
|
|
2610
|
-
this.recordingWebSocketMessages = true
|
|
2611
|
-
this.recordedWebSocketMessagesAtLeastOnce = true
|
|
2612
|
-
|
|
2613
|
-
this.cdpSession = await this.getNewCDPSession();
|
|
2614
|
-
await this.cdpSession.send('Network.enable');
|
|
2615
|
-
await this.cdpSession.send('Page.enable');
|
|
2616
|
-
|
|
2617
|
-
this.cdpSession.on(
|
|
2618
|
-
'Network.webSocketFrameReceived',
|
|
2619
|
-
(payload) => {
|
|
2620
|
-
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
2621
|
-
},
|
|
2622
|
-
);
|
|
2813
|
+
this.flushWebSocketMessages()
|
|
2814
|
+
this.recordingWebSocketMessages = true
|
|
2815
|
+
this.recordedWebSocketMessagesAtLeastOnce = true
|
|
2623
2816
|
|
|
2624
|
-
this.cdpSession.
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
2628
|
-
},
|
|
2629
|
-
);
|
|
2817
|
+
this.cdpSession = await this.getNewCDPSession()
|
|
2818
|
+
await this.cdpSession.send('Network.enable')
|
|
2819
|
+
await this.cdpSession.send('Page.enable')
|
|
2630
2820
|
|
|
2631
|
-
this.cdpSession.on(
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2821
|
+
this.cdpSession.on('Network.webSocketFrameReceived', payload => {
|
|
2822
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload))
|
|
2823
|
+
})
|
|
2824
|
+
|
|
2825
|
+
this.cdpSession.on('Network.webSocketFrameSent', payload => {
|
|
2826
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload))
|
|
2827
|
+
})
|
|
2828
|
+
|
|
2829
|
+
this.cdpSession.on('Network.webSocketFrameError', payload => {
|
|
2830
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload))
|
|
2831
|
+
})
|
|
2637
2832
|
}
|
|
2638
2833
|
|
|
2639
2834
|
/**
|
|
2640
2835
|
* {{> stopRecordingWebSocketMessages }}
|
|
2641
2836
|
*/
|
|
2642
2837
|
async stopRecordingWebSocketMessages() {
|
|
2643
|
-
await this.cdpSession.send('Network.disable')
|
|
2644
|
-
await this.cdpSession.send('Page.disable')
|
|
2645
|
-
this.page.removeAllListeners('Network')
|
|
2646
|
-
this.recordingWebSocketMessages = false
|
|
2838
|
+
await this.cdpSession.send('Network.disable')
|
|
2839
|
+
await this.cdpSession.send('Page.disable')
|
|
2840
|
+
this.page.removeAllListeners('Network')
|
|
2841
|
+
this.recordingWebSocketMessages = false
|
|
2647
2842
|
}
|
|
2648
2843
|
|
|
2649
2844
|
/**
|
|
@@ -2655,394 +2850,537 @@ class Puppeteer extends Helper {
|
|
|
2655
2850
|
grabWebSocketMessages() {
|
|
2656
2851
|
if (!this.recordingWebSocketMessages) {
|
|
2657
2852
|
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
2658
|
-
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
|
|
2853
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.')
|
|
2659
2854
|
}
|
|
2660
2855
|
}
|
|
2661
|
-
return this.webSocketMessages
|
|
2856
|
+
return this.webSocketMessages
|
|
2662
2857
|
}
|
|
2663
2858
|
|
|
2664
2859
|
/**
|
|
2665
2860
|
* Resets all recorded WS messages.
|
|
2666
2861
|
*/
|
|
2667
2862
|
flushWebSocketMessages() {
|
|
2668
|
-
this.webSocketMessages = []
|
|
2863
|
+
this.webSocketMessages = []
|
|
2669
2864
|
}
|
|
2670
2865
|
|
|
2671
2866
|
_getWebSocketMessage(payload) {
|
|
2672
2867
|
if (payload.errorMessage) {
|
|
2673
|
-
return payload.errorMessage
|
|
2868
|
+
return payload.errorMessage
|
|
2674
2869
|
}
|
|
2675
2870
|
|
|
2676
|
-
return payload.response.payloadData
|
|
2871
|
+
return payload.response.payloadData
|
|
2677
2872
|
}
|
|
2678
2873
|
|
|
2679
2874
|
_getWebSocketLog(prefix, payload) {
|
|
2680
|
-
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n
|
|
2875
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`
|
|
2681
2876
|
}
|
|
2682
2877
|
|
|
2683
2878
|
_logWebsocketMessages(message) {
|
|
2684
|
-
this.webSocketMessages
|
|
2879
|
+
this.webSocketMessages.push(message)
|
|
2685
2880
|
}
|
|
2686
2881
|
}
|
|
2687
2882
|
|
|
2688
|
-
export default Puppeteer
|
|
2883
|
+
export default Puppeteer
|
|
2689
2884
|
|
|
2885
|
+
/**
|
|
2886
|
+
* Find elements using Puppeteer's native element discovery methods
|
|
2887
|
+
* Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
|
|
2888
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2889
|
+
* @param {Object|string} locator - Locator specification
|
|
2890
|
+
* @returns {Promise<Array>} Array of ElementHandle objects
|
|
2891
|
+
*/
|
|
2690
2892
|
async function findElements(matcher, locator) {
|
|
2691
|
-
if
|
|
2692
|
-
|
|
2693
|
-
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
|
+
|
|
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)
|
|
2901
|
+
|
|
2902
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
|
|
2903
|
+
if (!locator.isXPath()) return matcher.$$(locator.simplify())
|
|
2904
|
+
|
|
2694
2905
|
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2695
2906
|
if (puppeteer.default?.defaultBrowserRevision) {
|
|
2696
|
-
return matcher.$$(`xpath/${locator.value}`)
|
|
2907
|
+
return matcher.$$(`xpath/${locator.value}`)
|
|
2908
|
+
}
|
|
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}`)
|
|
2697
2942
|
}
|
|
2698
|
-
|
|
2943
|
+
|
|
2944
|
+
// Fallback: return empty array
|
|
2945
|
+
return []
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* Find a single element using Puppeteer's native element discovery methods
|
|
2950
|
+
* Note: Puppeteer Locator API doesn't have .first() method like Playwright
|
|
2951
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2952
|
+
* @param {Object|string} locator - Locator specification
|
|
2953
|
+
* @returns {Promise<Object>} Single ElementHandle object
|
|
2954
|
+
*/
|
|
2955
|
+
async function findElement(matcher, locator) {
|
|
2956
|
+
if (locator.react) return findReactElements.call(this, locator)
|
|
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
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
|
|
2966
|
+
if (!locator.isXPath()) {
|
|
2967
|
+
const elements = await matcher.$$(locator.simplify())
|
|
2968
|
+
return elements[0]
|
|
2969
|
+
}
|
|
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)
|
|
2974
|
+
return elements[0]
|
|
2699
2975
|
}
|
|
2700
2976
|
|
|
2701
2977
|
async function proceedClick(locator, context = null, options = {}) {
|
|
2702
|
-
let matcher = await this.context
|
|
2978
|
+
let matcher = await this.context
|
|
2703
2979
|
if (context) {
|
|
2704
|
-
const els = await this._locate(context)
|
|
2705
|
-
assertElementExists(els, context)
|
|
2706
|
-
matcher = els[0]
|
|
2980
|
+
const els = await this._locate(context)
|
|
2981
|
+
assertElementExists(els, context)
|
|
2982
|
+
matcher = els[0]
|
|
2707
2983
|
}
|
|
2708
|
-
const els = await findClickable.call(this, matcher, locator)
|
|
2984
|
+
const els = await findClickable.call(this, matcher, locator)
|
|
2709
2985
|
if (context) {
|
|
2710
|
-
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
2986
|
+
assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`)
|
|
2711
2987
|
} else {
|
|
2712
|
-
assertElementExists(els, locator, 'Clickable element')
|
|
2988
|
+
assertElementExists(els, locator, 'Clickable element')
|
|
2713
2989
|
}
|
|
2714
2990
|
|
|
2715
|
-
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
2991
|
+
highlightActiveElement.call(this, els[0], await this._getContext())
|
|
2716
2992
|
|
|
2717
|
-
await els[0].click(options)
|
|
2718
|
-
const promises = []
|
|
2993
|
+
await els[0].click(options)
|
|
2994
|
+
const promises = []
|
|
2719
2995
|
if (options.waitForNavigation) {
|
|
2720
|
-
promises.push(this.waitForNavigation())
|
|
2996
|
+
promises.push(this.waitForNavigation())
|
|
2721
2997
|
}
|
|
2722
|
-
promises.push(this._waitForAction())
|
|
2998
|
+
promises.push(this._waitForAction())
|
|
2723
2999
|
|
|
2724
|
-
return Promise.all(promises)
|
|
3000
|
+
return Promise.all(promises)
|
|
2725
3001
|
}
|
|
2726
3002
|
|
|
2727
3003
|
async function findClickable(matcher, locator) {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
if (!
|
|
3004
|
+
const matchedLocator = new Locator(locator)
|
|
3005
|
+
|
|
3006
|
+
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
|
|
2731
3007
|
|
|
2732
|
-
let els
|
|
2733
|
-
const literal = xpathLocator.literal(
|
|
3008
|
+
let els
|
|
3009
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2734
3010
|
|
|
2735
|
-
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
2736
|
-
if (els.length) return els
|
|
3011
|
+
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
3012
|
+
if (els.length) return els
|
|
2737
3013
|
|
|
2738
|
-
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
2739
|
-
if (els.length) return els
|
|
3014
|
+
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
3015
|
+
if (els.length) return els
|
|
2740
3016
|
|
|
2741
3017
|
try {
|
|
2742
|
-
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
2743
|
-
if (els.length) return els
|
|
3018
|
+
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
3019
|
+
if (els.length) return els
|
|
2744
3020
|
} catch (err) {
|
|
2745
3021
|
// Do nothing
|
|
2746
3022
|
}
|
|
2747
3023
|
|
|
2748
|
-
|
|
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
|
|
2749
3033
|
}
|
|
2750
3034
|
|
|
2751
3035
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
2752
|
-
let description
|
|
2753
|
-
let allText
|
|
3036
|
+
let description
|
|
3037
|
+
let allText
|
|
2754
3038
|
if (!context) {
|
|
2755
|
-
let el = await this.context
|
|
3039
|
+
let el = await this.context
|
|
2756
3040
|
|
|
2757
3041
|
if (el && !el.getProperty) {
|
|
2758
3042
|
// Fallback to body
|
|
2759
|
-
el = await this.context.$('body')
|
|
3043
|
+
el = await this.context.$('body')
|
|
2760
3044
|
}
|
|
2761
3045
|
|
|
2762
|
-
allText = [await el.getProperty('innerText').then(p => p.jsonValue())]
|
|
2763
|
-
description = 'web application'
|
|
3046
|
+
allText = [await el.getProperty('innerText').then(p => p.jsonValue())]
|
|
3047
|
+
description = 'web application'
|
|
2764
3048
|
} else {
|
|
2765
|
-
const locator = new Locator(context, 'css')
|
|
2766
|
-
description = `element ${locator.toString()}
|
|
2767
|
-
const els = await this._locate(locator)
|
|
2768
|
-
assertElementExists(els, locator.toString())
|
|
2769
|
-
allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
|
|
3049
|
+
const locator = new Locator(context, 'css')
|
|
3050
|
+
description = `element ${locator.toString()}`
|
|
3051
|
+
const els = await this._locate(locator)
|
|
3052
|
+
assertElementExists(els, locator.toString())
|
|
3053
|
+
allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
if (store?.currentStep?.opts?.ignoreCase === true) {
|
|
3057
|
+
text = text.toLowerCase()
|
|
3058
|
+
allText = allText.map(elText => elText.toLowerCase())
|
|
2770
3059
|
}
|
|
2771
3060
|
|
|
2772
3061
|
if (strict) {
|
|
2773
|
-
return allText.map(elText => equals(description)[assertType](text, elText))
|
|
3062
|
+
return allText.map(elText => equals(description)[assertType](text, elText))
|
|
2774
3063
|
}
|
|
2775
|
-
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
|
|
3064
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')))
|
|
2776
3065
|
}
|
|
2777
3066
|
|
|
2778
3067
|
async function findCheckable(locator, context) {
|
|
2779
|
-
let contextEl = await this.context
|
|
3068
|
+
let contextEl = await this.context
|
|
2780
3069
|
if (typeof context === 'string') {
|
|
2781
|
-
contextEl = await findElements.call(this, contextEl,
|
|
2782
|
-
contextEl = contextEl[0]
|
|
3070
|
+
contextEl = await findElements.call(this, contextEl, new Locator(context, 'css').simplify())
|
|
3071
|
+
contextEl = contextEl[0]
|
|
2783
3072
|
}
|
|
2784
3073
|
|
|
2785
|
-
const matchedLocator = new Locator(locator)
|
|
3074
|
+
const matchedLocator = new Locator(locator)
|
|
2786
3075
|
if (!matchedLocator.isFuzzy()) {
|
|
2787
|
-
return findElements.call(this, contextEl, matchedLocator
|
|
3076
|
+
return findElements.call(this, contextEl, matchedLocator)
|
|
2788
3077
|
}
|
|
2789
3078
|
|
|
2790
|
-
const literal = xpathLocator.literal(
|
|
2791
|
-
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3079
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3080
|
+
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
2792
3081
|
if (els.length) {
|
|
2793
|
-
return els
|
|
3082
|
+
return els
|
|
2794
3083
|
}
|
|
2795
|
-
els = await findElements.call(this, contextEl, Locator.checkable.byName(literal))
|
|
3084
|
+
els = await findElements.call(this, contextEl, Locator.checkable.byName(literal))
|
|
2796
3085
|
if (els.length) {
|
|
2797
|
-
return els
|
|
3086
|
+
return els
|
|
3087
|
+
}
|
|
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
|
|
2798
3095
|
}
|
|
2799
|
-
|
|
3096
|
+
|
|
3097
|
+
return findElements.call(this, contextEl, matchedLocator.value)
|
|
2800
3098
|
}
|
|
2801
3099
|
|
|
2802
3100
|
async function proceedIsChecked(assertType, option) {
|
|
2803
|
-
let els = await findCheckable.call(this, option)
|
|
2804
|
-
assertElementExists(els, option, 'Checkable')
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3101
|
+
let els = await findCheckable.call(this, option)
|
|
3102
|
+
assertElementExists(els, option, 'Checkable')
|
|
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)
|
|
3121
|
+
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
2809
3122
|
}
|
|
2810
3123
|
|
|
2811
3124
|
async function findVisibleFields(locator) {
|
|
2812
|
-
const els = await findFields.call(this, locator)
|
|
2813
|
-
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
2814
|
-
return els.filter((el, index) => visible[index])
|
|
3125
|
+
const els = await findFields.call(this, locator)
|
|
3126
|
+
const visible = await Promise.all(els.map(el => el.boundingBox()))
|
|
3127
|
+
return els.filter((el, index) => visible[index])
|
|
2815
3128
|
}
|
|
2816
3129
|
|
|
2817
3130
|
async function findFields(locator) {
|
|
2818
|
-
const matchedLocator = new Locator(locator)
|
|
3131
|
+
const matchedLocator = new Locator(locator)
|
|
2819
3132
|
if (!matchedLocator.isFuzzy()) {
|
|
2820
|
-
return this._locate(matchedLocator)
|
|
3133
|
+
return this._locate(matchedLocator)
|
|
2821
3134
|
}
|
|
2822
|
-
const literal = xpathLocator.literal(
|
|
3135
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
2823
3136
|
|
|
2824
|
-
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
|
|
3137
|
+
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
|
|
2825
3138
|
if (els.length) {
|
|
2826
|
-
return els
|
|
3139
|
+
return els
|
|
2827
3140
|
}
|
|
2828
3141
|
|
|
2829
|
-
els = await this._locate({ xpath: Locator.field.labelContains(literal) })
|
|
3142
|
+
els = await this._locate({ xpath: Locator.field.labelContains(literal) })
|
|
2830
3143
|
if (els.length) {
|
|
2831
|
-
return els
|
|
3144
|
+
return els
|
|
2832
3145
|
}
|
|
2833
|
-
els = await this._locate({ xpath: Locator.field.byName(literal) })
|
|
3146
|
+
els = await this._locate({ xpath: Locator.field.byName(literal) })
|
|
2834
3147
|
if (els.length) {
|
|
2835
|
-
return els
|
|
3148
|
+
return els
|
|
3149
|
+
}
|
|
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
|
|
2836
3158
|
}
|
|
2837
|
-
|
|
3159
|
+
|
|
3160
|
+
return this._locate({ css: matchedLocator.value })
|
|
2838
3161
|
}
|
|
2839
3162
|
|
|
2840
3163
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
2841
|
-
const src = await this.
|
|
2842
|
-
|
|
3164
|
+
const src = await this._locateElement(sourceLocator)
|
|
3165
|
+
if (!src) {
|
|
3166
|
+
throw new ElementNotFound(sourceLocator, 'Source Element')
|
|
3167
|
+
}
|
|
2843
3168
|
|
|
2844
|
-
const dst = await this.
|
|
2845
|
-
|
|
3169
|
+
const dst = await this._locateElement(destinationLocator)
|
|
3170
|
+
if (!dst) {
|
|
3171
|
+
throw new ElementNotFound(destinationLocator, 'Destination Element')
|
|
3172
|
+
}
|
|
2846
3173
|
|
|
2847
|
-
// Note: Using public api .getClickablePoint
|
|
2848
|
-
const dragSource = await getClickablePoint(src
|
|
2849
|
-
const dragDestination = await getClickablePoint(dst
|
|
3174
|
+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
|
|
3175
|
+
const dragSource = await getClickablePoint(src)
|
|
3176
|
+
const dragDestination = await getClickablePoint(dst)
|
|
2850
3177
|
|
|
2851
3178
|
// Drag start point
|
|
2852
|
-
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
|
|
2853
|
-
await this.page.mouse.down()
|
|
3179
|
+
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
|
|
3180
|
+
await this.page.mouse.down()
|
|
2854
3181
|
|
|
2855
3182
|
// Drag destination
|
|
2856
|
-
await this.page.mouse.move(dragDestination.x, dragDestination.y, { steps: 5 })
|
|
2857
|
-
await this.page.mouse.up()
|
|
3183
|
+
await this.page.mouse.move(dragDestination.x, dragDestination.y, { steps: 5 })
|
|
3184
|
+
await this.page.mouse.up()
|
|
2858
3185
|
|
|
2859
|
-
await this._waitForAction()
|
|
3186
|
+
await this._waitForAction()
|
|
2860
3187
|
}
|
|
2861
3188
|
|
|
2862
3189
|
async function proceedSeeInField(assertType, field, value) {
|
|
2863
|
-
const els = await findVisibleFields.call(this, field)
|
|
2864
|
-
assertElementExists(els, field, 'Field')
|
|
2865
|
-
const el = els[0]
|
|
2866
|
-
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
2867
|
-
const fieldType = await el.getProperty('type').then(el => el.jsonValue())
|
|
3190
|
+
const els = await findVisibleFields.call(this, field)
|
|
3191
|
+
assertElementExists(els, field, 'Field')
|
|
3192
|
+
const el = els[0]
|
|
3193
|
+
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
3194
|
+
const fieldType = await el.getProperty('type').then(el => el.jsonValue())
|
|
2868
3195
|
|
|
2869
|
-
const proceedMultiple = async
|
|
2870
|
-
const fields = Array.isArray(elements) ? elements : [elements]
|
|
3196
|
+
const proceedMultiple = async elements => {
|
|
3197
|
+
const fields = Array.isArray(elements) ? elements : [elements]
|
|
2871
3198
|
|
|
2872
|
-
const elementValues = []
|
|
3199
|
+
const elementValues = []
|
|
2873
3200
|
for (const element of fields) {
|
|
2874
|
-
elementValues.push(await element.getProperty('value').then(el => el.jsonValue()))
|
|
3201
|
+
elementValues.push(await element.getProperty('value').then(el => el.jsonValue()))
|
|
2875
3202
|
}
|
|
2876
3203
|
|
|
2877
3204
|
if (typeof value === 'boolean') {
|
|
2878
|
-
equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length)
|
|
3205
|
+
equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length)
|
|
2879
3206
|
} else {
|
|
2880
3207
|
if (assertType === 'assert') {
|
|
2881
|
-
equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
|
|
3208
|
+
equals(`select option by ${field}`)[assertType](true, elementValues.length > 0)
|
|
2882
3209
|
}
|
|
2883
|
-
elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
|
|
3210
|
+
elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val))
|
|
2884
3211
|
}
|
|
2885
|
-
}
|
|
3212
|
+
}
|
|
2886
3213
|
|
|
2887
3214
|
if (tag === 'SELECT') {
|
|
2888
|
-
const selectedOptions = await el.$$('option:checked')
|
|
3215
|
+
const selectedOptions = await el.$$('option:checked')
|
|
2889
3216
|
// locate option by values and check them
|
|
2890
3217
|
if (value === '') {
|
|
2891
|
-
return proceedMultiple(selectedOptions)
|
|
3218
|
+
return proceedMultiple(selectedOptions)
|
|
2892
3219
|
}
|
|
2893
3220
|
|
|
2894
|
-
const options = await filterFieldsByValue(selectedOptions, value, true)
|
|
2895
|
-
return proceedMultiple(options)
|
|
3221
|
+
const options = await filterFieldsByValue(selectedOptions, value, true)
|
|
3222
|
+
return proceedMultiple(options)
|
|
2896
3223
|
}
|
|
2897
3224
|
|
|
2898
3225
|
if (tag === 'INPUT') {
|
|
2899
3226
|
if (fieldType === 'checkbox' || fieldType === 'radio') {
|
|
2900
3227
|
if (typeof value === 'boolean') {
|
|
2901
3228
|
// Filter by values
|
|
2902
|
-
const options = await filterFieldsBySelectionState(els, true)
|
|
2903
|
-
return proceedMultiple(options)
|
|
3229
|
+
const options = await filterFieldsBySelectionState(els, true)
|
|
3230
|
+
return proceedMultiple(options)
|
|
2904
3231
|
}
|
|
2905
3232
|
|
|
2906
|
-
const options = await filterFieldsByValue(els, value, true)
|
|
2907
|
-
return proceedMultiple(options)
|
|
3233
|
+
const options = await filterFieldsByValue(els, value, true)
|
|
3234
|
+
return proceedMultiple(options)
|
|
2908
3235
|
}
|
|
2909
|
-
return proceedMultiple(els[0])
|
|
3236
|
+
return proceedMultiple(els[0])
|
|
3237
|
+
}
|
|
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)
|
|
2910
3243
|
}
|
|
2911
|
-
|
|
2912
|
-
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
|
|
3244
|
+
|
|
3245
|
+
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal)
|
|
2913
3246
|
}
|
|
2914
3247
|
|
|
2915
3248
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
2916
|
-
const matches = []
|
|
3249
|
+
const matches = []
|
|
2917
3250
|
for (const element of elements) {
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
+
|
|
3257
|
+
let isSelected = true
|
|
2920
3258
|
if (onlySelected) {
|
|
2921
|
-
isSelected = await elementSelected(element)
|
|
3259
|
+
isSelected = await elementSelected(element)
|
|
2922
3260
|
}
|
|
2923
|
-
if ((value == null || val.indexOf(value) > -1) && isSelected) {
|
|
2924
|
-
matches.push(element)
|
|
3261
|
+
if ((value == null || (val && val.indexOf(value) > -1)) && isSelected) {
|
|
3262
|
+
matches.push(element)
|
|
2925
3263
|
}
|
|
2926
3264
|
}
|
|
2927
|
-
return matches
|
|
3265
|
+
return matches
|
|
2928
3266
|
}
|
|
2929
3267
|
|
|
2930
3268
|
async function filterFieldsBySelectionState(elements, state) {
|
|
2931
|
-
const matches = []
|
|
3269
|
+
const matches = []
|
|
2932
3270
|
for (const element of elements) {
|
|
2933
|
-
const isSelected = await elementSelected(element)
|
|
3271
|
+
const isSelected = await elementSelected(element)
|
|
2934
3272
|
if (isSelected === state) {
|
|
2935
|
-
matches.push(element)
|
|
3273
|
+
matches.push(element)
|
|
2936
3274
|
}
|
|
2937
3275
|
}
|
|
2938
|
-
return matches
|
|
3276
|
+
return matches
|
|
2939
3277
|
}
|
|
2940
3278
|
|
|
2941
3279
|
async function elementSelected(element) {
|
|
2942
|
-
const type = await element.getProperty('type').then(el => el.jsonValue())
|
|
3280
|
+
const type = await element.getProperty('type').then(el => el.jsonValue())
|
|
2943
3281
|
|
|
2944
3282
|
if (type === 'checkbox' || type === 'radio') {
|
|
2945
|
-
return element.getProperty('checked').then(el => el.jsonValue())
|
|
3283
|
+
return element.getProperty('checked').then(el => el.jsonValue())
|
|
2946
3284
|
}
|
|
2947
|
-
return element.getProperty('selected').then(el => el.jsonValue())
|
|
3285
|
+
return element.getProperty('selected').then(el => el.jsonValue())
|
|
2948
3286
|
}
|
|
2949
3287
|
|
|
2950
3288
|
function isFrameLocator(locator) {
|
|
2951
|
-
locator = new Locator(locator)
|
|
3289
|
+
locator = new Locator(locator)
|
|
2952
3290
|
if (locator.isFrame()) {
|
|
2953
|
-
const _locator = new Locator(locator)
|
|
2954
|
-
return _locator.value
|
|
3291
|
+
const _locator = new Locator(locator)
|
|
3292
|
+
return _locator.value
|
|
2955
3293
|
}
|
|
2956
|
-
return false
|
|
3294
|
+
return false
|
|
2957
3295
|
}
|
|
2958
3296
|
|
|
2959
3297
|
function assertElementExists(res, locator, prefix, suffix) {
|
|
2960
3298
|
if (!res || res.length === 0) {
|
|
2961
|
-
throw new ElementNotFound(locator, prefix, suffix)
|
|
3299
|
+
throw new ElementNotFound(locator, prefix, suffix)
|
|
2962
3300
|
}
|
|
2963
3301
|
}
|
|
2964
3302
|
|
|
2965
3303
|
function $XPath(element, selector) {
|
|
2966
|
-
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
2967
|
-
const res = []
|
|
2968
|
-
let current = null
|
|
2969
|
-
while (current = found.iterateNext()) {
|
|
2970
|
-
res.push(current)
|
|
3304
|
+
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
3305
|
+
const res = []
|
|
3306
|
+
let current = null
|
|
3307
|
+
while ((current = found.iterateNext())) {
|
|
3308
|
+
res.push(current)
|
|
2971
3309
|
}
|
|
2972
|
-
return res
|
|
3310
|
+
return res
|
|
2973
3311
|
}
|
|
2974
3312
|
|
|
2975
3313
|
async function targetCreatedHandler(page) {
|
|
2976
|
-
if (!page) return
|
|
2977
|
-
this.withinLocator = null
|
|
3314
|
+
if (!page) return
|
|
3315
|
+
this.withinLocator = null
|
|
2978
3316
|
page.on('load', () => {
|
|
2979
|
-
page
|
|
3317
|
+
page
|
|
3318
|
+
.$('body')
|
|
2980
3319
|
.catch(() => null)
|
|
2981
|
-
.then(context => this.context = context)
|
|
2982
|
-
})
|
|
2983
|
-
page.on('console',
|
|
2984
|
-
this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '))
|
|
2985
|
-
consoleLogStore.add(msg)
|
|
2986
|
-
})
|
|
3320
|
+
.then(context => (this.context = context))
|
|
3321
|
+
})
|
|
3322
|
+
page.on('console', msg => {
|
|
3323
|
+
this.debugSection(`Browser:${ucfirst(msg.type())}`, (msg._text || '') + msg.args().join(' '))
|
|
3324
|
+
consoleLogStore.add(msg)
|
|
3325
|
+
})
|
|
2987
3326
|
|
|
2988
3327
|
if (this.options.userAgent) {
|
|
2989
|
-
await page.setUserAgent(this.options.userAgent)
|
|
3328
|
+
await page.setUserAgent(this.options.userAgent)
|
|
2990
3329
|
}
|
|
2991
3330
|
if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
|
|
2992
|
-
const dimensions = this.options.windowSize.split('x')
|
|
2993
|
-
const width = parseInt(dimensions[0], 10)
|
|
2994
|
-
const height = parseInt(dimensions[1], 10)
|
|
2995
|
-
await page.setViewport({ width, height })
|
|
3331
|
+
const dimensions = this.options.windowSize.split('x')
|
|
3332
|
+
const width = parseInt(dimensions[0], 10)
|
|
3333
|
+
const height = parseInt(dimensions[1], 10)
|
|
3334
|
+
await page.setViewport({ width, height })
|
|
2996
3335
|
}
|
|
2997
3336
|
}
|
|
2998
3337
|
|
|
2999
3338
|
// BC compatibility for Puppeteer < 10
|
|
3000
3339
|
async function getClickablePoint(el) {
|
|
3001
|
-
if (el.clickablePoint) return el.clickablePoint()
|
|
3002
|
-
if (el._clickablePoint) return el._clickablePoint()
|
|
3003
|
-
return null
|
|
3340
|
+
if (el.clickablePoint) return el.clickablePoint()
|
|
3341
|
+
if (el._clickablePoint) return el._clickablePoint()
|
|
3342
|
+
return null
|
|
3004
3343
|
}
|
|
3005
3344
|
|
|
3006
3345
|
// List of key values to key definitions
|
|
3007
|
-
// https://github.com/
|
|
3346
|
+
// https://github.com/puppeteer/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
|
|
3008
3347
|
const keyDefinitionMap = {
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
'z': 'KeyZ',
|
|
3348
|
+
0: 'Digit0',
|
|
3349
|
+
1: 'Digit1',
|
|
3350
|
+
2: 'Digit2',
|
|
3351
|
+
3: 'Digit3',
|
|
3352
|
+
4: 'Digit4',
|
|
3353
|
+
5: 'Digit5',
|
|
3354
|
+
6: 'Digit6',
|
|
3355
|
+
7: 'Digit7',
|
|
3356
|
+
8: 'Digit8',
|
|
3357
|
+
9: 'Digit9',
|
|
3358
|
+
a: 'KeyA',
|
|
3359
|
+
b: 'KeyB',
|
|
3360
|
+
c: 'KeyC',
|
|
3361
|
+
d: 'KeyD',
|
|
3362
|
+
e: 'KeyE',
|
|
3363
|
+
f: 'KeyF',
|
|
3364
|
+
g: 'KeyG',
|
|
3365
|
+
h: 'KeyH',
|
|
3366
|
+
i: 'KeyI',
|
|
3367
|
+
j: 'KeyJ',
|
|
3368
|
+
k: 'KeyK',
|
|
3369
|
+
l: 'KeyL',
|
|
3370
|
+
m: 'KeyM',
|
|
3371
|
+
n: 'KeyN',
|
|
3372
|
+
o: 'KeyO',
|
|
3373
|
+
p: 'KeyP',
|
|
3374
|
+
q: 'KeyQ',
|
|
3375
|
+
r: 'KeyR',
|
|
3376
|
+
s: 'KeyS',
|
|
3377
|
+
t: 'KeyT',
|
|
3378
|
+
u: 'KeyU',
|
|
3379
|
+
v: 'KeyV',
|
|
3380
|
+
w: 'KeyW',
|
|
3381
|
+
x: 'KeyX',
|
|
3382
|
+
y: 'KeyY',
|
|
3383
|
+
z: 'KeyZ',
|
|
3046
3384
|
';': 'Semicolon',
|
|
3047
3385
|
'=': 'Equal',
|
|
3048
3386
|
',': 'Comma',
|
|
@@ -3053,91 +3391,150 @@ const keyDefinitionMap = {
|
|
|
3053
3391
|
'[': 'BracketLeft',
|
|
3054
3392
|
'\\': 'Backslash',
|
|
3055
3393
|
']': 'BracketRight',
|
|
3056
|
-
'
|
|
3057
|
-
|
|
3058
|
-
};
|
|
3394
|
+
"'": 'Quote',
|
|
3395
|
+
}
|
|
3059
3396
|
|
|
3060
3397
|
function getNormalizedKey(key) {
|
|
3061
|
-
const normalizedKey = getNormalizedKeyAttributeValue(key)
|
|
3398
|
+
const normalizedKey = getNormalizedKeyAttributeValue(key)
|
|
3062
3399
|
if (key !== normalizedKey) {
|
|
3063
|
-
this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`)
|
|
3400
|
+
this.debugSection('Input', `Mapping key '${key}' to '${normalizedKey}'`)
|
|
3064
3401
|
}
|
|
3065
3402
|
// Use key definition to ensure correct key is displayed when Shift modifier is active
|
|
3066
3403
|
if (Object.prototype.hasOwnProperty.call(keyDefinitionMap, normalizedKey)) {
|
|
3067
|
-
return keyDefinitionMap[normalizedKey]
|
|
3404
|
+
return keyDefinitionMap[normalizedKey]
|
|
3068
3405
|
}
|
|
3069
|
-
return normalizedKey
|
|
3406
|
+
return normalizedKey
|
|
3070
3407
|
}
|
|
3071
3408
|
|
|
3072
3409
|
function highlightActiveElement(element, context) {
|
|
3073
3410
|
if (this.options.highlightElement && global.debugMode) {
|
|
3074
|
-
highlightElement(element, context)
|
|
3411
|
+
highlightElement(element, context)
|
|
3075
3412
|
}
|
|
3076
3413
|
}
|
|
3077
3414
|
|
|
3078
3415
|
function _waitForElement(locator, options) {
|
|
3079
3416
|
try {
|
|
3080
|
-
return this.context.waitForXPath(locator.value, options)
|
|
3417
|
+
return this.context.waitForXPath(locator.value, options)
|
|
3081
3418
|
} catch (e) {
|
|
3082
|
-
return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options)
|
|
3419
|
+
return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options)
|
|
3083
3420
|
}
|
|
3084
3421
|
}
|
|
3085
3422
|
|
|
3086
|
-
async function findReactElements(locator
|
|
3087
|
-
|
|
3088
|
-
|
|
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)
|
|
3431
|
+
const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8')
|
|
3432
|
+
await this.page.evaluate(resqScript.toString())
|
|
3433
|
+
|
|
3434
|
+
await this.page.evaluate(() => window.resq.waitToLoadReact())
|
|
3435
|
+
const arrayHandle = await this.page.evaluateHandle(
|
|
3436
|
+
obj => {
|
|
3437
|
+
const { selector, props, state } = obj
|
|
3438
|
+
let elements = window.resq.resq$$(selector)
|
|
3439
|
+
if (Object.keys(props).length) {
|
|
3440
|
+
elements = elements.byProps(props)
|
|
3441
|
+
}
|
|
3442
|
+
if (Object.keys(state).length) {
|
|
3443
|
+
elements = elements.byState(state)
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
if (!elements.length) {
|
|
3447
|
+
return []
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
// resq returns an array of HTMLElements if the React component is a fragment
|
|
3451
|
+
// this avoids having nested arrays of nodes which the driver does not understand
|
|
3452
|
+
// [[div, div], [div, div]] => [div, div, div, div]
|
|
3453
|
+
let nodes = []
|
|
3089
3454
|
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
const { selector, props, state } = obj;
|
|
3093
|
-
let elements = window.resq.resq$$(selector);
|
|
3094
|
-
if (Object.keys(props).length) {
|
|
3095
|
-
elements = elements.byProps(props);
|
|
3096
|
-
}
|
|
3097
|
-
if (Object.keys(state).length) {
|
|
3098
|
-
elements = elements.byState(state);
|
|
3099
|
-
}
|
|
3455
|
+
elements.forEach(element => {
|
|
3456
|
+
let { node, isFragment } = element
|
|
3100
3457
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3458
|
+
if (!node) {
|
|
3459
|
+
isFragment = true
|
|
3460
|
+
node = element.children
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
if (isFragment) {
|
|
3464
|
+
nodes = nodes.concat(node)
|
|
3465
|
+
} else {
|
|
3466
|
+
nodes.push(node)
|
|
3467
|
+
}
|
|
3468
|
+
})
|
|
3469
|
+
|
|
3470
|
+
return [...nodes]
|
|
3471
|
+
},
|
|
3472
|
+
{
|
|
3473
|
+
selector: resolved.react,
|
|
3474
|
+
props: resolved.props || {},
|
|
3475
|
+
state: resolved.state || {},
|
|
3476
|
+
},
|
|
3477
|
+
)
|
|
3478
|
+
|
|
3479
|
+
const properties = await arrayHandle.getProperties()
|
|
3480
|
+
const result = []
|
|
3481
|
+
for (const property of properties.values()) {
|
|
3482
|
+
const elementHandle = property.asElement()
|
|
3483
|
+
if (elementHandle) {
|
|
3484
|
+
result.push(elementHandle)
|
|
3103
3485
|
}
|
|
3486
|
+
}
|
|
3104
3487
|
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
let nodes = [];
|
|
3488
|
+
await arrayHandle.dispose()
|
|
3489
|
+
return result
|
|
3490
|
+
}
|
|
3109
3491
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3492
|
+
async function findByRole(matcher, locator) {
|
|
3493
|
+
const resolved = toLocatorConfig(locator, 'role')
|
|
3494
|
+
const roleSelector = buildRoleSelector(resolved)
|
|
3112
3495
|
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
}
|
|
3496
|
+
if (!resolved.text && !resolved.name) {
|
|
3497
|
+
return matcher.$$(roleSelector)
|
|
3498
|
+
}
|
|
3117
3499
|
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
});
|
|
3500
|
+
const allElements = await matcher.$$(roleSelector)
|
|
3501
|
+
const filtered = []
|
|
3502
|
+
const accessibleName = resolved.text ?? resolved.name
|
|
3503
|
+
const matcherFn = createRoleTextMatcher(accessibleName, resolved.exact === true)
|
|
3124
3504
|
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
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
|
+
})
|
|
3131
3513
|
|
|
3132
|
-
|
|
3133
|
-
const result = [];
|
|
3134
|
-
for (const property of properties.values()) {
|
|
3135
|
-
const elementHandle = property.asElement();
|
|
3136
|
-
if (elementHandle) {
|
|
3137
|
-
result.push(elementHandle);
|
|
3138
|
-
}
|
|
3514
|
+
if (texts.some(text => matcherFn(text))) filtered.push(el)
|
|
3139
3515
|
}
|
|
3140
3516
|
|
|
3141
|
-
|
|
3142
|
-
|
|
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)
|
|
3143
3539
|
}
|
|
3540
|
+
|