codeceptjs 3.5.0 → 3.5.1-2.beta.7
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 +24 -25
- package/lib/actor.js +6 -3
- package/lib/ai.js +12 -3
- package/lib/cli.js +12 -2
- package/lib/codecept.js +4 -0
- package/lib/colorUtils.js +10 -0
- package/lib/command/definitions.js +2 -7
- package/lib/command/dryRun.js +2 -1
- package/lib/command/info.js +24 -0
- package/lib/command/init.js +51 -5
- package/lib/command/run-multiple/collection.js +17 -5
- package/lib/command/run-multiple.js +4 -2
- package/lib/command/run-workers.js +66 -4
- package/lib/command/run.js +7 -0
- package/lib/command/workers/runTests.js +39 -0
- package/lib/data/context.js +14 -6
- package/lib/event.js +4 -0
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +73 -24
- package/lib/helper/Expect.js +422 -0
- package/lib/helper/FileSystem.js +1 -1
- package/lib/helper/GraphQL.js +25 -0
- package/lib/helper/Nightmare.js +9 -4
- package/lib/helper/OpenAI.js +14 -10
- package/lib/helper/Playwright.js +1205 -288
- package/lib/helper/Protractor.js +11 -6
- package/lib/helper/Puppeteer.js +173 -61
- package/lib/helper/TestCafe.js +44 -9
- package/lib/helper/WebDriver.js +231 -82
- package/lib/helper/errors/ElementNotFound.js +2 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
- package/lib/helper/scripts/blurElement.js +17 -0
- package/lib/helper/scripts/focusElement.js +17 -0
- package/lib/helper/scripts/highlightElement.js +2 -2
- package/lib/html.js +3 -3
- package/lib/interfaces/bdd.js +1 -1
- package/lib/interfaces/gherkin.js +37 -3
- package/lib/interfaces/scenarioConfig.js +1 -0
- package/lib/locator.js +17 -4
- package/lib/mochaFactory.js +2 -1
- package/lib/output.js +1 -1
- package/lib/pause.js +12 -9
- package/lib/plugin/autoLogin.js +45 -10
- package/lib/plugin/heal.js +47 -17
- package/lib/plugin/retryFailedStep.js +10 -1
- package/lib/plugin/retryTo.js +2 -4
- package/lib/plugin/selenoid.js +6 -1
- package/lib/plugin/standardActingHelpers.js +0 -2
- package/lib/plugin/stepByStepReport.js +2 -2
- package/lib/plugin/tryTo.js +5 -7
- package/lib/plugin/wdio.js +0 -1
- package/lib/recorder.js +20 -9
- package/lib/session.js +1 -1
- package/lib/step.js +30 -11
- package/lib/ui.js +1 -0
- package/lib/utils.js +18 -1
- package/lib/workers.js +28 -3
- package/package.json +108 -98
- package/translations/de-DE.js +5 -0
- package/translations/fr-FR.js +14 -1
- package/translations/it-IT.js +1 -0
- package/translations/ja-JP.js +5 -0
- package/translations/pl-PL.js +5 -0
- package/translations/pt-BR.js +1 -0
- package/translations/ru-RU.js +1 -0
- package/translations/zh-CN.js +5 -0
- package/translations/zh-TW.js +5 -0
- package/typings/index.d.ts +8 -6
- package/typings/promiseBasedTypes.d.ts +784 -822
- package/typings/types.d.ts +1214 -727
- package/CHANGELOG.md +0 -2492
- package/docs/advanced.md +0 -351
- package/docs/ai.md +0 -246
- package/docs/api.md +0 -323
- package/docs/basics.md +0 -980
- package/docs/bdd.md +0 -535
- package/docs/best.md +0 -237
- package/docs/books.md +0 -37
- package/docs/bootstrap.md +0 -135
- package/docs/build/ApiDataFactory.js +0 -409
- package/docs/build/Appium.js +0 -1978
- package/docs/build/FileSystem.js +0 -228
- package/docs/build/GraphQL.js +0 -204
- package/docs/build/GraphQLDataFactory.js +0 -309
- package/docs/build/JSONResponse.js +0 -338
- package/docs/build/Mochawesome.js +0 -71
- package/docs/build/Nightmare.js +0 -2147
- package/docs/build/OpenAI.js +0 -122
- package/docs/build/Playwright.js +0 -4134
- package/docs/build/Polly.js +0 -42
- package/docs/build/Protractor.js +0 -2701
- package/docs/build/Puppeteer.js +0 -3743
- package/docs/build/REST.js +0 -344
- package/docs/build/SeleniumWebdriver.js +0 -76
- package/docs/build/TestCafe.js +0 -2059
- package/docs/build/WebDriver.js +0 -4042
- package/docs/changelog.md +0 -2501
- package/docs/commands.md +0 -254
- package/docs/community-helpers.md +0 -58
- package/docs/configuration.md +0 -157
- package/docs/continuous-integration.md +0 -22
- package/docs/custom-helpers.md +0 -306
- package/docs/data.md +0 -375
- package/docs/detox.md +0 -235
- package/docs/docker.md +0 -137
- package/docs/email.md +0 -183
- package/docs/examples.md +0 -149
- package/docs/helpers/ApiDataFactory.md +0 -266
- package/docs/helpers/Appium.md +0 -1317
- package/docs/helpers/Detox.md +0 -586
- package/docs/helpers/FileSystem.md +0 -152
- package/docs/helpers/GraphQL.md +0 -130
- package/docs/helpers/GraphQLDataFactory.md +0 -226
- package/docs/helpers/JSONResponse.md +0 -254
- package/docs/helpers/Mochawesome.md +0 -8
- package/docs/helpers/MockRequest.md +0 -377
- package/docs/helpers/Nightmare.md +0 -1258
- package/docs/helpers/OpenAI.md +0 -70
- package/docs/helpers/Playwright.md +0 -2250
- package/docs/helpers/Polly.md +0 -44
- package/docs/helpers/Puppeteer-firefox.md +0 -86
- package/docs/helpers/Puppeteer.md +0 -2147
- package/docs/helpers/REST.md +0 -218
- package/docs/helpers/TestCafe.md +0 -1224
- package/docs/helpers/WebDriver.md +0 -2325
- package/docs/hooks.md +0 -340
- package/docs/index.md +0 -111
- package/docs/installation.md +0 -75
- package/docs/internal-api.md +0 -265
- package/docs/locators.md +0 -331
- package/docs/mobile-react-native-locators.md +0 -67
- package/docs/mobile.md +0 -344
- package/docs/nightmare.md +0 -223
- package/docs/pageobjects.md +0 -291
- package/docs/parallel.md +0 -288
- package/docs/playwright.md +0 -609
- package/docs/plugins.md +0 -1225
- package/docs/puppeteer.md +0 -316
- package/docs/quickstart.md +0 -163
- package/docs/react.md +0 -69
- package/docs/reports.md +0 -392
- package/docs/secrets.md +0 -36
- package/docs/shadow.md +0 -68
- package/docs/shared/keys.mustache +0 -31
- package/docs/shared/react.mustache +0 -1
- package/docs/testcafe.md +0 -174
- package/docs/translation.md +0 -247
- package/docs/tutorial.md +0 -271
- package/docs/typescript.md +0 -180
- package/docs/ui.md +0 -59
- package/docs/videos.md +0 -28
- package/docs/visual.md +0 -202
- package/docs/vue.md +0 -121
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -10
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -25
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/docs/webdriver.md +0 -657
- package/docs/wiki/Books-&-Posts.md +0 -27
- package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
- package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
- package/docs/wiki/Examples.md +0 -139
- package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
- package/docs/wiki/Home.md +0 -16
- package/docs/wiki/Release-Process.md +0 -24
- package/docs/wiki/Roadmap.md +0 -23
- package/docs/wiki/Tests.md +0 -1393
- package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
- package/docs/wiki/Videos.md +0 -19
package/lib/helper/Playwright.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
|
|
4
4
|
const Helper = require('@codeceptjs/helper');
|
|
5
5
|
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
const assert = require('assert');
|
|
6
7
|
const Locator = require('../locator');
|
|
7
8
|
const store = require('../store');
|
|
8
9
|
const recorder = require('../recorder');
|
|
@@ -22,6 +23,7 @@ const {
|
|
|
22
23
|
isModifierKey,
|
|
23
24
|
clearString,
|
|
24
25
|
requireWithFallback,
|
|
26
|
+
normalizeSpacesInString,
|
|
25
27
|
} = require('../utils');
|
|
26
28
|
const {
|
|
27
29
|
isColorProperty,
|
|
@@ -31,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
31
33
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
32
34
|
const Popup = require('./extras/Popup');
|
|
33
35
|
const Console = require('./extras/Console');
|
|
34
|
-
const findReact = require('./extras/
|
|
36
|
+
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
|
|
35
37
|
|
|
36
38
|
let playwright;
|
|
37
39
|
let perfTiming;
|
|
@@ -45,20 +47,19 @@ const {
|
|
|
45
47
|
setRestartStrategy, restartsSession, restartsContext, restartsBrowser,
|
|
46
48
|
} = require('./extras/PlaywrightRestartOpts');
|
|
47
49
|
const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine');
|
|
48
|
-
const { highlightElement } = require('./scripts/highlightElement');
|
|
49
50
|
|
|
50
51
|
const pathSeparator = path.sep;
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* ## Configuration
|
|
54
55
|
*
|
|
55
|
-
* This helper should be configured in codecept.conf.js
|
|
56
|
+
* This helper should be configured in codecept.conf.(js|ts)
|
|
56
57
|
*
|
|
57
58
|
* @typedef PlaywrightConfig
|
|
58
59
|
* @type {object}
|
|
59
|
-
* @prop {string} url - base url of website to be tested
|
|
60
|
+
* @prop {string} [url] - base url of website to be tested
|
|
60
61
|
* @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
|
|
61
|
-
* @prop {boolean} [show=
|
|
62
|
+
* @prop {boolean} [show=true] - show browser window.
|
|
62
63
|
* @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
|
|
63
64
|
* * 'context' or **false** - restarts [browser context](https://playwright.dev/docs/api/class-browsercontext) but keeps running browser. Recommended by Playwright team to keep tests isolated.
|
|
64
65
|
* * 'browser' or **true** - closes browser and opens it again between tests.
|
|
@@ -75,7 +76,7 @@ const pathSeparator = path.sep;
|
|
|
75
76
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
|
|
76
77
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
|
|
77
78
|
* @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
78
|
-
* @prop {'load' | 'domcontentloaded' | '
|
|
79
|
+
* @prop {'load' | 'domcontentloaded' | 'commit'} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `commit`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-url).
|
|
79
80
|
* @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
|
|
80
81
|
* @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
|
|
81
82
|
* @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
|
|
@@ -92,7 +93,8 @@ const pathSeparator = path.sep;
|
|
|
92
93
|
* @prop {string[]} [ignoreLog] - An array with console message types that are not logged to debug log. Default value is `['warning', 'log']`. E.g. you can set `[]` to log all messages. See all possible [values](https://playwright.dev/docs/api/class-consolemessage#console-message-type).
|
|
93
94
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
94
95
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
95
|
-
* @prop {boolean} [highlightElement] - highlight the interacting elements
|
|
96
|
+
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
97
|
+
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
96
98
|
*/
|
|
97
99
|
const config = {};
|
|
98
100
|
|
|
@@ -115,6 +117,10 @@ const config = {};
|
|
|
115
117
|
* npm i playwright-core@^1.18 --save
|
|
116
118
|
* ```
|
|
117
119
|
*
|
|
120
|
+
* Breaking Changes: if you use Playwright v1.38 and later, it will no longer download browsers automatically.
|
|
121
|
+
*
|
|
122
|
+
* Run `npx playwright install` to download browsers after `npm install`.
|
|
123
|
+
*
|
|
118
124
|
* Using playwright-core package, will prevent the download of browser binaries and allow connecting to an existing browser installation or for connecting to a remote one.
|
|
119
125
|
*
|
|
120
126
|
*
|
|
@@ -136,6 +142,21 @@ const config = {};
|
|
|
136
142
|
* * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
|
|
137
143
|
* * `keepTraceForPassedTests`: - save trace for passed tests
|
|
138
144
|
*
|
|
145
|
+
* #### HAR Recording Customization
|
|
146
|
+
*
|
|
147
|
+
* A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded.
|
|
148
|
+
* It contains information about the request and response headers, cookies, content, timings, and more. You can use HAR files to mock network requests in your tests.
|
|
149
|
+
* HAR will be saved to `output/har`. More info could be found here https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har.
|
|
150
|
+
*
|
|
151
|
+
* ```
|
|
152
|
+
* ...
|
|
153
|
+
* recordHar: {
|
|
154
|
+
* mode: 'minimal', // possible values: 'minimal'|'full'.
|
|
155
|
+
* content: 'embed' // possible values: "omit"|"embed"|"attach".
|
|
156
|
+
* }
|
|
157
|
+
* ...
|
|
158
|
+
*```
|
|
159
|
+
*
|
|
139
160
|
* #### Example #1: Wait for 0 network connections.
|
|
140
161
|
*
|
|
141
162
|
* ```js
|
|
@@ -206,6 +227,7 @@ const config = {};
|
|
|
206
227
|
* url: "http://localhost",
|
|
207
228
|
* show: true // headless mode not supported for extensions
|
|
208
229
|
* chromium: {
|
|
230
|
+
* // Note: due to this would launch persistent context, so to avoid the error when running tests with run-workers a timestamp would be appended to the defined folder name. For instance: playwright-tmp_1692715649511
|
|
209
231
|
* userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
|
|
210
232
|
* args: [
|
|
211
233
|
* `--disable-extensions-except=${pathToExtension}`,
|
|
@@ -260,6 +282,22 @@ const config = {};
|
|
|
260
282
|
* }
|
|
261
283
|
* ```
|
|
262
284
|
*
|
|
285
|
+
* * #### Example #9: Launch electron test
|
|
286
|
+
*
|
|
287
|
+
* ```js
|
|
288
|
+
* {
|
|
289
|
+
* helpers: {
|
|
290
|
+
* Playwright: {
|
|
291
|
+
* browser: 'electron',
|
|
292
|
+
* electron: {
|
|
293
|
+
* executablePath: require("electron"),
|
|
294
|
+
* args: [path.join('../', "main.js")],
|
|
295
|
+
* },
|
|
296
|
+
* }
|
|
297
|
+
* },
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
263
301
|
* Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
|
|
264
302
|
*
|
|
265
303
|
* ## Access From Helpers
|
|
@@ -295,6 +333,17 @@ class Playwright extends Helper {
|
|
|
295
333
|
this.electronSessions = [];
|
|
296
334
|
this.storageState = null;
|
|
297
335
|
|
|
336
|
+
// for network stuff
|
|
337
|
+
this.requests = [];
|
|
338
|
+
this.recording = false;
|
|
339
|
+
this.recordedAtLeastOnce = false;
|
|
340
|
+
|
|
341
|
+
// for websocket messages
|
|
342
|
+
this.webSocketMessages = [];
|
|
343
|
+
this.recordingWebSocketMessages = false;
|
|
344
|
+
this.recordedWebSocketMessagesAtLeastOnce = false;
|
|
345
|
+
this.cdpSession = null;
|
|
346
|
+
|
|
298
347
|
// override defaults with config
|
|
299
348
|
this._setConfig(config);
|
|
300
349
|
}
|
|
@@ -313,7 +362,7 @@ class Playwright extends Helper {
|
|
|
313
362
|
ignoreLog: ['warning', 'log'],
|
|
314
363
|
uniqueScreenshotNames: false,
|
|
315
364
|
manualStart: false,
|
|
316
|
-
getPageTimeout:
|
|
365
|
+
getPageTimeout: 30000,
|
|
317
366
|
waitForNavigation: 'load',
|
|
318
367
|
restart: false,
|
|
319
368
|
keepCookies: false,
|
|
@@ -321,7 +370,8 @@ class Playwright extends Helper {
|
|
|
321
370
|
show: false,
|
|
322
371
|
defaultPopupAction: 'accept',
|
|
323
372
|
use: { actionTimeout: 0 },
|
|
324
|
-
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors
|
|
373
|
+
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
374
|
+
highlightElement: false,
|
|
325
375
|
};
|
|
326
376
|
|
|
327
377
|
config = Object.assign(defaults, config);
|
|
@@ -366,22 +416,31 @@ class Playwright extends Helper {
|
|
|
366
416
|
}
|
|
367
417
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
|
|
368
418
|
this.isElectron = this.options.browser === 'electron';
|
|
369
|
-
this.userDataDir = this.playwrightOptions.userDataDir;
|
|
419
|
+
this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined;
|
|
370
420
|
this.isCDPConnection = this.playwrightOptions.cdpConnection;
|
|
371
421
|
popupStore.defaultAction = this.options.defaultPopupAction;
|
|
372
422
|
}
|
|
373
423
|
|
|
374
424
|
static _config() {
|
|
375
425
|
return [
|
|
376
|
-
{ name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
|
|
377
|
-
{
|
|
378
|
-
name: 'show', message: 'Show browser window', default: true, type: 'confirm',
|
|
379
|
-
},
|
|
380
426
|
{
|
|
381
427
|
name: 'browser',
|
|
382
428
|
message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
|
|
383
429
|
default: 'chromium',
|
|
384
430
|
},
|
|
431
|
+
{
|
|
432
|
+
name: 'url',
|
|
433
|
+
message: 'Base url of site to be tested',
|
|
434
|
+
default: 'http://localhost',
|
|
435
|
+
when: (answers) => answers.Playwright_browser !== 'electron',
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'show',
|
|
439
|
+
message: 'Show browser window',
|
|
440
|
+
default: true,
|
|
441
|
+
type: 'confirm',
|
|
442
|
+
when: (answers) => answers.Playwright_browser !== 'electron',
|
|
443
|
+
},
|
|
385
444
|
];
|
|
386
445
|
}
|
|
387
446
|
|
|
@@ -412,9 +471,10 @@ class Playwright extends Helper {
|
|
|
412
471
|
}
|
|
413
472
|
}
|
|
414
473
|
|
|
415
|
-
async _before() {
|
|
474
|
+
async _before(test) {
|
|
475
|
+
this.currentRunningTest = test;
|
|
416
476
|
recorder.retry({
|
|
417
|
-
retries:
|
|
477
|
+
retries: process.env.FAILED_STEP_RETRIES || 3,
|
|
418
478
|
when: err => {
|
|
419
479
|
if (!err || typeof (err.message) !== 'string') {
|
|
420
480
|
return false;
|
|
@@ -430,7 +490,7 @@ class Playwright extends Helper {
|
|
|
430
490
|
this.isAuthenticated = false;
|
|
431
491
|
if (this.isElectron) {
|
|
432
492
|
this.browserContext = this.browser.context();
|
|
433
|
-
} else if (this.userDataDir) {
|
|
493
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
434
494
|
this.browserContext = this.browser;
|
|
435
495
|
} else {
|
|
436
496
|
const contextOptions = {
|
|
@@ -442,13 +502,24 @@ class Playwright extends Helper {
|
|
|
442
502
|
contextOptions.httpCredentials = this.options.basicAuth;
|
|
443
503
|
this.isAuthenticated = true;
|
|
444
504
|
}
|
|
505
|
+
if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
|
|
445
506
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
|
|
507
|
+
if (this.options.recordHar) {
|
|
508
|
+
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har';
|
|
509
|
+
const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`;
|
|
510
|
+
const dir = path.dirname(fileName);
|
|
511
|
+
if (!fileExists(dir)) fs.mkdirSync(dir);
|
|
512
|
+
this.options.recordHar.path = fileName;
|
|
513
|
+
this.currentRunningTest.artifacts.har = fileName;
|
|
514
|
+
contextOptions.recordHar = this.options.recordHar;
|
|
515
|
+
}
|
|
446
516
|
if (this.storageState) contextOptions.storageState = this.storageState;
|
|
447
517
|
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
|
|
448
518
|
if (this.options.locale) contextOptions.locale = this.options.locale;
|
|
449
519
|
if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme;
|
|
520
|
+
this.contextOptions = contextOptions;
|
|
450
521
|
if (!this.browserContext || !restartsSession()) {
|
|
451
|
-
this.browserContext = await this.browser.newContext(contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
|
|
522
|
+
this.browserContext = await this.browser.newContext(this.contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors
|
|
452
523
|
}
|
|
453
524
|
}
|
|
454
525
|
|
|
@@ -456,8 +527,17 @@ class Playwright extends Helper {
|
|
|
456
527
|
if (this.isElectron) {
|
|
457
528
|
mainPage = await this.browser.firstWindow();
|
|
458
529
|
} else {
|
|
459
|
-
|
|
460
|
-
|
|
530
|
+
try {
|
|
531
|
+
const existingPages = await this.browserContext.pages();
|
|
532
|
+
mainPage = existingPages[0] || await this.browserContext.newPage();
|
|
533
|
+
} catch (e) {
|
|
534
|
+
if (this.playwrightOptions.userDataDir) {
|
|
535
|
+
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
536
|
+
this.browserContext = this.browser;
|
|
537
|
+
const existingPages = await this.browserContext.pages();
|
|
538
|
+
mainPage = existingPages[0];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
461
541
|
}
|
|
462
542
|
await targetCreatedHandler.call(this, mainPage);
|
|
463
543
|
|
|
@@ -488,13 +568,15 @@ class Playwright extends Helper {
|
|
|
488
568
|
|
|
489
569
|
// close other sessions
|
|
490
570
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
this.
|
|
495
|
-
|
|
571
|
+
if ((await this.browser)._type === 'Browser') {
|
|
572
|
+
const contexts = await this.browser.contexts();
|
|
573
|
+
const currentContext = contexts[0];
|
|
574
|
+
if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
|
|
575
|
+
this.storageState = await currentContext.storageState();
|
|
576
|
+
}
|
|
496
577
|
|
|
497
|
-
|
|
578
|
+
await Promise.all(contexts.map(c => c.close()));
|
|
579
|
+
}
|
|
498
580
|
} catch (e) {
|
|
499
581
|
console.log(e);
|
|
500
582
|
}
|
|
@@ -524,8 +606,16 @@ class Playwright extends Helper {
|
|
|
524
606
|
browserContext = browser.context();
|
|
525
607
|
page = await browser.firstWindow();
|
|
526
608
|
} else {
|
|
527
|
-
|
|
528
|
-
|
|
609
|
+
try {
|
|
610
|
+
browserContext = await this.browser.newContext(Object.assign(this.contextOptions, config));
|
|
611
|
+
page = await browserContext.newPage();
|
|
612
|
+
} catch (e) {
|
|
613
|
+
if (this.playwrightOptions.userDataDir) {
|
|
614
|
+
browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions);
|
|
615
|
+
this.browser = browserContext;
|
|
616
|
+
page = await browserContext.pages()[0];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
529
619
|
}
|
|
530
620
|
|
|
531
621
|
if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
|
|
@@ -538,10 +628,12 @@ class Playwright extends Helper {
|
|
|
538
628
|
// is closed by _after
|
|
539
629
|
},
|
|
540
630
|
loadVars: async (context) => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
631
|
+
if (context) {
|
|
632
|
+
this.browserContext = context;
|
|
633
|
+
const existingPages = await context.pages();
|
|
634
|
+
this.sessionPages[this.activeSessionName] = existingPages[0];
|
|
635
|
+
return this._setPage(this.sessionPages[this.activeSessionName]);
|
|
636
|
+
}
|
|
545
637
|
},
|
|
546
638
|
restoreVars: async (session) => {
|
|
547
639
|
this.withinLocator = null;
|
|
@@ -575,7 +667,7 @@ class Playwright extends Helper {
|
|
|
575
667
|
* ```
|
|
576
668
|
*
|
|
577
669
|
* @param {string} description used to show in logs.
|
|
578
|
-
* @param {function} fn async function that executed with Playwright helper as
|
|
670
|
+
* @param {function} fn async function that executed with Playwright helper as arguments
|
|
579
671
|
*/
|
|
580
672
|
usePlaywrightTo(description, fn) {
|
|
581
673
|
return this._useTo(...arguments);
|
|
@@ -732,7 +824,7 @@ class Playwright extends Helper {
|
|
|
732
824
|
}
|
|
733
825
|
throw err;
|
|
734
826
|
}
|
|
735
|
-
} else if (this.userDataDir) {
|
|
827
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
736
828
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
737
829
|
} else {
|
|
738
830
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -765,9 +857,11 @@ class Playwright extends Helper {
|
|
|
765
857
|
|
|
766
858
|
async _stopBrowser() {
|
|
767
859
|
this.withinLocator = null;
|
|
768
|
-
this._setPage(null);
|
|
860
|
+
await this._setPage(null);
|
|
769
861
|
this.context = null;
|
|
862
|
+
this.frame = null;
|
|
770
863
|
popupStore.clear();
|
|
864
|
+
if (this.options.recordHar) await this.browserContext.close();
|
|
771
865
|
await this.browser.close();
|
|
772
866
|
}
|
|
773
867
|
|
|
@@ -788,14 +882,14 @@ class Playwright extends Helper {
|
|
|
788
882
|
await this.switchTo(null);
|
|
789
883
|
return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
|
|
790
884
|
}
|
|
791
|
-
await this.switchTo(
|
|
792
|
-
this.withinLocator = new Locator(
|
|
885
|
+
await this.switchTo(frame);
|
|
886
|
+
this.withinLocator = new Locator(frame);
|
|
793
887
|
return;
|
|
794
888
|
}
|
|
795
889
|
|
|
796
|
-
const
|
|
797
|
-
assertElementExists(
|
|
798
|
-
this.context =
|
|
890
|
+
const el = await this._locateElement(locator);
|
|
891
|
+
assertElementExists(el, locator);
|
|
892
|
+
this.context = el;
|
|
799
893
|
this.contextLocator = locator;
|
|
800
894
|
|
|
801
895
|
this.withinLocator = new Locator(locator);
|
|
@@ -805,6 +899,7 @@ class Playwright extends Helper {
|
|
|
805
899
|
this.withinLocator = null;
|
|
806
900
|
this.context = await this.page;
|
|
807
901
|
this.contextLocator = null;
|
|
902
|
+
this.frame = null;
|
|
808
903
|
}
|
|
809
904
|
|
|
810
905
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
@@ -852,10 +947,9 @@ class Playwright extends Helper {
|
|
|
852
947
|
}
|
|
853
948
|
|
|
854
949
|
/**
|
|
855
|
-
* {{> resizeWindow }}
|
|
856
950
|
*
|
|
857
951
|
* Unlike other drivers Playwright changes the size of a viewport, not the window!
|
|
858
|
-
* Playwright does not control the window of a browser so it can't adjust its real size.
|
|
952
|
+
* Playwright does not control the window of a browser, so it can't adjust its real size.
|
|
859
953
|
* It also can't maximize a window.
|
|
860
954
|
*
|
|
861
955
|
* Update configuration to change real window size on start:
|
|
@@ -865,6 +959,8 @@ class Playwright extends Helper {
|
|
|
865
959
|
* // @codeceptjs/configure package must be installed
|
|
866
960
|
* { setWindowSize } = require('@codeceptjs/configure');
|
|
867
961
|
* ````
|
|
962
|
+
*
|
|
963
|
+
* {{> resizeWindow }}
|
|
868
964
|
*/
|
|
869
965
|
async resizeWindow(width, height) {
|
|
870
966
|
if (width === 'maximize') {
|
|
@@ -879,14 +975,14 @@ class Playwright extends Helper {
|
|
|
879
975
|
* Set headers for all next requests
|
|
880
976
|
*
|
|
881
977
|
* ```js
|
|
882
|
-
* I.
|
|
978
|
+
* I.setPlaywrightRequestHeaders({
|
|
883
979
|
* 'X-Sent-By': 'CodeceptJS',
|
|
884
980
|
* });
|
|
885
981
|
* ```
|
|
886
982
|
*
|
|
887
983
|
* @param {object} customHeaders headers to set
|
|
888
984
|
*/
|
|
889
|
-
async
|
|
985
|
+
async setPlaywrightRequestHeaders(customHeaders) {
|
|
890
986
|
if (!customHeaders) {
|
|
891
987
|
throw new Error('Cannot send empty headers.');
|
|
892
988
|
}
|
|
@@ -898,70 +994,72 @@ class Playwright extends Helper {
|
|
|
898
994
|
*
|
|
899
995
|
*/
|
|
900
996
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
901
|
-
const
|
|
902
|
-
assertElementExists(
|
|
997
|
+
const el = await this._locateElement(locator);
|
|
998
|
+
assertElementExists(el, locator);
|
|
903
999
|
|
|
904
1000
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
905
|
-
const { x, y } = await clickablePoint(
|
|
1001
|
+
const { x, y } = await clickablePoint(el);
|
|
906
1002
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
907
1003
|
return this._waitForAction();
|
|
908
1004
|
}
|
|
909
1005
|
|
|
910
1006
|
/**
|
|
911
|
-
*
|
|
912
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
913
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
|
|
914
|
-
*
|
|
915
|
-
* Examples:
|
|
916
|
-
*
|
|
917
|
-
* ```js
|
|
918
|
-
* I.dontSee('#add-to-cart-btn');
|
|
919
|
-
* I.focus('#product-tile')
|
|
920
|
-
* I.see('#add-to-cart-bnt');
|
|
921
|
-
* ```
|
|
1007
|
+
* {{> focus }}
|
|
922
1008
|
*
|
|
923
1009
|
*/
|
|
924
1010
|
async focus(locator, options = {}) {
|
|
925
|
-
const
|
|
926
|
-
assertElementExists(
|
|
927
|
-
const el = els[0];
|
|
1011
|
+
const el = await this._locateElement(locator);
|
|
1012
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
928
1013
|
|
|
929
1014
|
await el.focus(options);
|
|
930
1015
|
return this._waitForAction();
|
|
931
1016
|
}
|
|
932
1017
|
|
|
933
1018
|
/**
|
|
934
|
-
*
|
|
935
|
-
* Calls [blur](https://playwright.dev/docs/api/class-locator#locator-blur) on the element.
|
|
936
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
937
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
|
|
938
|
-
*
|
|
939
|
-
* Examples:
|
|
940
|
-
*
|
|
941
|
-
* ```js
|
|
942
|
-
* I.blur('.text-area')
|
|
943
|
-
* ```
|
|
944
|
-
* ```js
|
|
945
|
-
* //element `#product-tile` is focused
|
|
946
|
-
* I.see('#add-to-cart-btn');
|
|
947
|
-
* I.blur('#product-tile')
|
|
948
|
-
* I.dontSee('#add-to-cart-btn');
|
|
949
|
-
* ```
|
|
1019
|
+
* {{> blur }}
|
|
950
1020
|
*
|
|
951
1021
|
*/
|
|
952
1022
|
async blur(locator, options = {}) {
|
|
953
|
-
const
|
|
954
|
-
assertElementExists(
|
|
955
|
-
// TODO: locator change required after #3677 implementation
|
|
956
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1023
|
+
const el = await this._locateElement(locator);
|
|
1024
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
957
1025
|
|
|
958
|
-
await
|
|
1026
|
+
await el.blur(options);
|
|
959
1027
|
return this._waitForAction();
|
|
960
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Return the checked status of given element.
|
|
1031
|
+
*
|
|
1032
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
1033
|
+
* @param {object} [options] See https://playwright.dev/docs/api/class-locator#locator-is-checked
|
|
1034
|
+
* @return {Promise<boolean>}
|
|
1035
|
+
*
|
|
1036
|
+
*/
|
|
1037
|
+
|
|
1038
|
+
async grabCheckedElementStatus(locator, options = {}) {
|
|
1039
|
+
const supportedTypes = ['checkbox', 'radio'];
|
|
1040
|
+
const el = await this._locateElement(locator);
|
|
1041
|
+
const type = await el.getAttribute('type');
|
|
1042
|
+
|
|
1043
|
+
if (supportedTypes.includes(type)) {
|
|
1044
|
+
return el.isChecked(options);
|
|
1045
|
+
}
|
|
1046
|
+
throw new Error(`Element is not a ${supportedTypes.join(' or ')} input`);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Return the disabled status of given element.
|
|
1050
|
+
*
|
|
1051
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
1052
|
+
* @param {object} [options] See https://playwright.dev/docs/api/class-locator#locator-is-disabled
|
|
1053
|
+
* @return {Promise<boolean>}
|
|
1054
|
+
*
|
|
1055
|
+
*/
|
|
1056
|
+
|
|
1057
|
+
async grabDisabledElementStatus(locator, options = {}) {
|
|
1058
|
+
const el = await this._locateElement(locator);
|
|
1059
|
+
return el.isDisabled(options);
|
|
1060
|
+
}
|
|
961
1061
|
|
|
962
1062
|
/**
|
|
963
|
-
* {{> dragAndDrop }}
|
|
964
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
|
|
965
1063
|
*
|
|
966
1064
|
* ```js
|
|
967
1065
|
* // specify coordinates for source position
|
|
@@ -969,6 +1067,10 @@ class Playwright extends Helper {
|
|
|
969
1067
|
* ```
|
|
970
1068
|
*
|
|
971
1069
|
* > When no option is set, custom drag and drop would be used, to use the dragAndDrop API from Playwright, please set options, for example `force: true`
|
|
1070
|
+
*
|
|
1071
|
+
* {{> dragAndDrop }}
|
|
1072
|
+
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
|
|
1073
|
+
*
|
|
972
1074
|
*/
|
|
973
1075
|
async dragAndDrop(srcElement, destElement, options) {
|
|
974
1076
|
const src = new Locator(srcElement);
|
|
@@ -1018,6 +1120,33 @@ class Playwright extends Helper {
|
|
|
1018
1120
|
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
|
|
1019
1121
|
}
|
|
1020
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Replaying from HAR
|
|
1125
|
+
*
|
|
1126
|
+
* ```js
|
|
1127
|
+
* // Replay API requests from HAR.
|
|
1128
|
+
* // Either use a matching response from the HAR,
|
|
1129
|
+
* // or abort the request if nothing matches.
|
|
1130
|
+
* I.replayFromHar('./output/har/something.har', { url: "*\/**\/api/v1/fruits" });
|
|
1131
|
+
* I.amOnPage('https://demo.playwright.dev/api-mocking');
|
|
1132
|
+
* I.see('CodeceptJS');
|
|
1133
|
+
* ```
|
|
1134
|
+
*
|
|
1135
|
+
* @param {string} harFilePath Path to recorded HAR file
|
|
1136
|
+
* @param {object} [opts] [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har)
|
|
1137
|
+
*
|
|
1138
|
+
* @returns Promise<void>
|
|
1139
|
+
*/
|
|
1140
|
+
async replayFromHar(harFilePath, opts) {
|
|
1141
|
+
const file = path.join(global.codecept_dir, harFilePath);
|
|
1142
|
+
|
|
1143
|
+
if (!fileExists(file)) {
|
|
1144
|
+
throw new Error(`File at ${file} cannot be found on local system`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
await this.page.routeFromHAR(harFilePath, opts);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1021
1150
|
/**
|
|
1022
1151
|
* {{> scrollPageToTop }}
|
|
1023
1152
|
*/
|
|
@@ -1035,8 +1164,11 @@ class Playwright extends Helper {
|
|
|
1035
1164
|
const body = document.body;
|
|
1036
1165
|
const html = document.documentElement;
|
|
1037
1166
|
window.scrollTo(0, Math.max(
|
|
1038
|
-
body.scrollHeight,
|
|
1039
|
-
|
|
1167
|
+
body.scrollHeight,
|
|
1168
|
+
body.offsetHeight,
|
|
1169
|
+
html.clientHeight,
|
|
1170
|
+
html.scrollHeight,
|
|
1171
|
+
html.offsetHeight,
|
|
1040
1172
|
));
|
|
1041
1173
|
});
|
|
1042
1174
|
}
|
|
@@ -1052,10 +1184,10 @@ class Playwright extends Helper {
|
|
|
1052
1184
|
}
|
|
1053
1185
|
|
|
1054
1186
|
if (locator) {
|
|
1055
|
-
const
|
|
1056
|
-
assertElementExists(
|
|
1057
|
-
await
|
|
1058
|
-
const elementCoordinates = await clickablePoint(
|
|
1187
|
+
const el = await this._locateElement(locator);
|
|
1188
|
+
assertElementExists(el, locator, 'Element');
|
|
1189
|
+
await el.scrollIntoViewIfNeeded();
|
|
1190
|
+
const elementCoordinates = await clickablePoint(el);
|
|
1059
1191
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
1060
1192
|
} else {
|
|
1061
1193
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1119,11 +1251,27 @@ class Playwright extends Helper {
|
|
|
1119
1251
|
*/
|
|
1120
1252
|
async _locate(locator) {
|
|
1121
1253
|
const context = await this.context || await this._getContext();
|
|
1254
|
+
|
|
1255
|
+
if (this.frame) return findElements(this.frame, locator);
|
|
1256
|
+
|
|
1122
1257
|
return findElements(context, locator);
|
|
1123
1258
|
}
|
|
1124
1259
|
|
|
1125
1260
|
/**
|
|
1126
|
-
*
|
|
1261
|
+
* Get the first element by different locator types, including strict locator
|
|
1262
|
+
* Should be used in custom helpers:
|
|
1263
|
+
*
|
|
1264
|
+
* ```js
|
|
1265
|
+
* const element = await this.helpers['Playwright']._locateElement({name: 'password'});
|
|
1266
|
+
* ```
|
|
1267
|
+
*/
|
|
1268
|
+
async _locateElement(locator) {
|
|
1269
|
+
const context = await this.context || await this._getContext();
|
|
1270
|
+
return findElement(context, locator);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Find a checkbox by providing human-readable text:
|
|
1127
1275
|
* NOTE: Assumes the checkable element exists
|
|
1128
1276
|
*
|
|
1129
1277
|
* ```js
|
|
@@ -1138,7 +1286,7 @@ class Playwright extends Helper {
|
|
|
1138
1286
|
}
|
|
1139
1287
|
|
|
1140
1288
|
/**
|
|
1141
|
-
* Find a clickable element by providing human
|
|
1289
|
+
* Find a clickable element by providing human-readable text:
|
|
1142
1290
|
*
|
|
1143
1291
|
* ```js
|
|
1144
1292
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1150,7 +1298,7 @@ class Playwright extends Helper {
|
|
|
1150
1298
|
}
|
|
1151
1299
|
|
|
1152
1300
|
/**
|
|
1153
|
-
* Find field elements by providing human
|
|
1301
|
+
* Find field elements by providing human-readable text:
|
|
1154
1302
|
*
|
|
1155
1303
|
* ```js
|
|
1156
1304
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1160,6 +1308,22 @@ class Playwright extends Helper {
|
|
|
1160
1308
|
return findFields.call(this, locator);
|
|
1161
1309
|
}
|
|
1162
1310
|
|
|
1311
|
+
/**
|
|
1312
|
+
* {{> grabWebElements }}
|
|
1313
|
+
*
|
|
1314
|
+
*/
|
|
1315
|
+
async grabWebElements(locator) {
|
|
1316
|
+
return this._locate(locator);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* {{> grabWebElement }}
|
|
1321
|
+
*
|
|
1322
|
+
*/
|
|
1323
|
+
async grabWebElement(locator) {
|
|
1324
|
+
return this._locateElement(locator);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1163
1327
|
/**
|
|
1164
1328
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1165
1329
|
*
|
|
@@ -1354,7 +1518,7 @@ class Playwright extends Helper {
|
|
|
1354
1518
|
*
|
|
1355
1519
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
|
|
1356
1520
|
*
|
|
1357
|
-
*
|
|
1521
|
+
* @example
|
|
1358
1522
|
*
|
|
1359
1523
|
* ```js
|
|
1360
1524
|
* // click on element at position
|
|
@@ -1387,8 +1551,6 @@ class Playwright extends Helper {
|
|
|
1387
1551
|
|
|
1388
1552
|
/**
|
|
1389
1553
|
* {{> doubleClick }}
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
1554
|
*/
|
|
1393
1555
|
async doubleClick(locator, context = null) {
|
|
1394
1556
|
return proceedClick.call(this, locator, context, { clickCount: 2 });
|
|
@@ -1396,15 +1558,12 @@ class Playwright extends Helper {
|
|
|
1396
1558
|
|
|
1397
1559
|
/**
|
|
1398
1560
|
* {{> rightClick }}
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
1561
|
*/
|
|
1402
1562
|
async rightClick(locator, context = null) {
|
|
1403
1563
|
return proceedClick.call(this, locator, context, { button: 'right' });
|
|
1404
1564
|
}
|
|
1405
1565
|
|
|
1406
1566
|
/**
|
|
1407
|
-
* {{> checkOption }}
|
|
1408
1567
|
*
|
|
1409
1568
|
* [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-check) for check available as 3rd argument.
|
|
1410
1569
|
*
|
|
@@ -1415,6 +1574,9 @@ class Playwright extends Helper {
|
|
|
1415
1574
|
* I.checkOption('Agree', '.signup', { position: { x: 5, y: 5 } })
|
|
1416
1575
|
* ```
|
|
1417
1576
|
* > ⚠️ To avoid flakiness, option `force: true` is set by default
|
|
1577
|
+
*
|
|
1578
|
+
* {{> checkOption }}
|
|
1579
|
+
*
|
|
1418
1580
|
*/
|
|
1419
1581
|
async checkOption(field, context = null, options = { force: true }) {
|
|
1420
1582
|
const elm = await this._locateCheckable(field, context);
|
|
@@ -1423,7 +1585,6 @@ class Playwright extends Helper {
|
|
|
1423
1585
|
}
|
|
1424
1586
|
|
|
1425
1587
|
/**
|
|
1426
|
-
* {{> uncheckOption }}
|
|
1427
1588
|
*
|
|
1428
1589
|
* [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-uncheck) for uncheck available as 3rd argument.
|
|
1429
1590
|
*
|
|
@@ -1434,6 +1595,8 @@ class Playwright extends Helper {
|
|
|
1434
1595
|
* I.uncheckOption('Agree', '.signup', { position: { x: 5, y: 5 } })
|
|
1435
1596
|
* ```
|
|
1436
1597
|
* > ⚠️ To avoid flakiness, option `force: true` is set by default
|
|
1598
|
+
*
|
|
1599
|
+
* {{> uncheckOption }}
|
|
1437
1600
|
*/
|
|
1438
1601
|
async uncheckOption(field, context = null, options = { force: true }) {
|
|
1439
1602
|
const elm = await this._locateCheckable(field, context);
|
|
@@ -1474,9 +1637,10 @@ class Playwright extends Helper {
|
|
|
1474
1637
|
}
|
|
1475
1638
|
|
|
1476
1639
|
/**
|
|
1477
|
-
* {{> pressKeyWithKeyNormalization }}
|
|
1478
1640
|
*
|
|
1479
1641
|
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
|
|
1642
|
+
*
|
|
1643
|
+
* {{> pressKeyWithKeyNormalization }}
|
|
1480
1644
|
*/
|
|
1481
1645
|
async pressKey(key) {
|
|
1482
1646
|
const modifiers = [];
|
|
@@ -1526,15 +1690,10 @@ class Playwright extends Helper {
|
|
|
1526
1690
|
const els = await findFields.call(this, field);
|
|
1527
1691
|
assertElementExists(els, field, 'Field');
|
|
1528
1692
|
const el = els[0];
|
|
1529
|
-
const tag = await el.getProperty('tagName').then(el => el.jsonValue());
|
|
1530
|
-
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
|
|
1531
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1532
|
-
await this._evaluateHandeInContext(el => el.value = '', el);
|
|
1533
|
-
} else if (editable) {
|
|
1534
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1535
|
-
}
|
|
1536
1693
|
|
|
1537
|
-
|
|
1694
|
+
await el.clear();
|
|
1695
|
+
|
|
1696
|
+
await highlightActiveElement.call(this, el);
|
|
1538
1697
|
|
|
1539
1698
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1540
1699
|
|
|
@@ -1542,46 +1701,42 @@ class Playwright extends Helper {
|
|
|
1542
1701
|
}
|
|
1543
1702
|
|
|
1544
1703
|
/**
|
|
1545
|
-
*
|
|
1704
|
+
* Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
|
|
1705
|
+
*
|
|
1706
|
+
*
|
|
1707
|
+
* Examples:
|
|
1708
|
+
*
|
|
1709
|
+
* ```js
|
|
1710
|
+
* I.clearField('.text-area')
|
|
1711
|
+
*
|
|
1712
|
+
* // if this doesn't work use force option
|
|
1713
|
+
* I.clearField('#submit', { force: true })
|
|
1714
|
+
* ```
|
|
1715
|
+
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
1716
|
+
*
|
|
1546
1717
|
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
1547
1718
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1548
|
-
*
|
|
1549
|
-
* Examples:
|
|
1550
|
-
*
|
|
1551
|
-
* ```js
|
|
1552
|
-
* I.clearField('.text-area')
|
|
1553
|
-
* ```
|
|
1554
|
-
* ```js
|
|
1555
|
-
* I.clearField('#submit', { force: true }) // force to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
1556
|
-
* ```
|
|
1557
1719
|
*/
|
|
1558
1720
|
async clearField(locator, options = {}) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1721
|
+
const els = await findFields.call(this, locator);
|
|
1722
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
1723
|
+
|
|
1724
|
+
const el = els[0];
|
|
1561
1725
|
|
|
1562
|
-
|
|
1563
|
-
const els = await findFields.call(this, locator);
|
|
1564
|
-
assertElementExists(els, locator, 'Field to clear');
|
|
1565
|
-
// TODO: locator change required after #3677 implementation
|
|
1566
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1726
|
+
await highlightActiveElement.call(this, el);
|
|
1567
1727
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
result = await this.fillField(locator, '');
|
|
1572
|
-
}
|
|
1573
|
-
return result;
|
|
1728
|
+
await el.clear();
|
|
1729
|
+
|
|
1730
|
+
return this._waitForAction();
|
|
1574
1731
|
}
|
|
1575
1732
|
|
|
1576
1733
|
/**
|
|
1577
1734
|
* {{> appendField }}
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
1580
1735
|
*/
|
|
1581
1736
|
async appendField(field, value) {
|
|
1582
1737
|
const els = await findFields.call(this, field);
|
|
1583
1738
|
assertElementExists(els, field, 'Field');
|
|
1584
|
-
highlightActiveElement.call(this, els[0]
|
|
1739
|
+
await highlightActiveElement.call(this, els[0]);
|
|
1585
1740
|
await els[0].press('End');
|
|
1586
1741
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1587
1742
|
return this._waitForAction();
|
|
@@ -1591,14 +1746,16 @@ class Playwright extends Helper {
|
|
|
1591
1746
|
* {{> seeInField }}
|
|
1592
1747
|
*/
|
|
1593
1748
|
async seeInField(field, value) {
|
|
1594
|
-
|
|
1749
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1750
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
1595
1751
|
}
|
|
1596
1752
|
|
|
1597
1753
|
/**
|
|
1598
1754
|
* {{> dontSeeInField }}
|
|
1599
1755
|
*/
|
|
1600
1756
|
async dontSeeInField(field, value) {
|
|
1601
|
-
|
|
1757
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1758
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
1602
1759
|
}
|
|
1603
1760
|
|
|
1604
1761
|
/**
|
|
@@ -1624,29 +1781,19 @@ class Playwright extends Helper {
|
|
|
1624
1781
|
const els = await findFields.call(this, select);
|
|
1625
1782
|
assertElementExists(els, select, 'Selectable field');
|
|
1626
1783
|
const el = els[0];
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
|
|
1636
|
-
if (optEl.length) {
|
|
1637
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
|
|
1641
|
-
if (optEl.length) {
|
|
1642
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1643
|
-
}
|
|
1784
|
+
|
|
1785
|
+
await highlightActiveElement.call(this, el);
|
|
1786
|
+
let optionToSelect = '';
|
|
1787
|
+
|
|
1788
|
+
try {
|
|
1789
|
+
optionToSelect = await el.locator('option', { hasText: option }).textContent();
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
optionToSelect = option;
|
|
1644
1792
|
}
|
|
1645
|
-
await this._evaluateHandeInContext((element) => {
|
|
1646
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1647
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1648
|
-
}, el);
|
|
1649
1793
|
|
|
1794
|
+
if (!Array.isArray(option)) option = [optionToSelect];
|
|
1795
|
+
|
|
1796
|
+
await el.selectOption(option);
|
|
1650
1797
|
return this._waitForAction();
|
|
1651
1798
|
}
|
|
1652
1799
|
|
|
@@ -1808,9 +1955,9 @@ class Playwright extends Helper {
|
|
|
1808
1955
|
}
|
|
1809
1956
|
|
|
1810
1957
|
/**
|
|
1811
|
-
* {{> grabCookie }}
|
|
1812
|
-
*
|
|
1813
1958
|
* Returns cookie in JSON format. If name not passed returns all cookies for this domain.
|
|
1959
|
+
*
|
|
1960
|
+
* {{> grabCookie }}
|
|
1814
1961
|
*/
|
|
1815
1962
|
async grabCookie(name) {
|
|
1816
1963
|
const cookies = await this.browserContext.cookies();
|
|
@@ -1824,7 +1971,7 @@ class Playwright extends Helper {
|
|
|
1824
1971
|
*/
|
|
1825
1972
|
async clearCookie() {
|
|
1826
1973
|
// Playwright currently doesn't support to delete a certain cookie
|
|
1827
|
-
// https://github.com/microsoft/playwright/blob/main/docs/api.md#
|
|
1974
|
+
// https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies
|
|
1828
1975
|
if (!this.browserContext) return;
|
|
1829
1976
|
return this.browserContext.clearCookies();
|
|
1830
1977
|
}
|
|
@@ -1841,8 +1988,8 @@ class Playwright extends Helper {
|
|
|
1841
1988
|
* ```js
|
|
1842
1989
|
* I.executeScript(({x, y}) => x + y, {x, y});
|
|
1843
1990
|
* ```
|
|
1844
|
-
* You can pass only one parameter into a function
|
|
1845
|
-
*
|
|
1991
|
+
* You can pass only one parameter into a function,
|
|
1992
|
+
* or you can pass in array or object.
|
|
1846
1993
|
*
|
|
1847
1994
|
* ```js
|
|
1848
1995
|
* I.executeScript(([x, y]) => x + y, [x, y]);
|
|
@@ -1854,11 +2001,11 @@ class Playwright extends Helper {
|
|
|
1854
2001
|
* @returns {Promise<any>}
|
|
1855
2002
|
*/
|
|
1856
2003
|
async executeScript(fn, arg) {
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2004
|
+
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2005
|
+
// switching to iframe context
|
|
2006
|
+
return this.context.locator(':root').evaluate(fn, arg);
|
|
1860
2007
|
}
|
|
1861
|
-
return
|
|
2008
|
+
return this.page.evaluate.apply(this.page, [fn, arg]);
|
|
1862
2009
|
}
|
|
1863
2010
|
|
|
1864
2011
|
/**
|
|
@@ -1897,7 +2044,7 @@ class Playwright extends Helper {
|
|
|
1897
2044
|
const els = await this._locate(locator);
|
|
1898
2045
|
const texts = [];
|
|
1899
2046
|
for (const el of els) {
|
|
1900
|
-
texts.push(await (await el.
|
|
2047
|
+
texts.push(await (await el.innerText()));
|
|
1901
2048
|
}
|
|
1902
2049
|
this.debug(`Matched ${els.length} elements`);
|
|
1903
2050
|
return texts;
|
|
@@ -1919,7 +2066,7 @@ class Playwright extends Helper {
|
|
|
1919
2066
|
async grabValueFromAll(locator) {
|
|
1920
2067
|
const els = await findFields.call(this, locator);
|
|
1921
2068
|
this.debug(`Matched ${els.length} elements`);
|
|
1922
|
-
return Promise.all(els.map(el => el.
|
|
2069
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
1923
2070
|
}
|
|
1924
2071
|
|
|
1925
2072
|
/**
|
|
@@ -1938,7 +2085,7 @@ class Playwright extends Helper {
|
|
|
1938
2085
|
async grabHTMLFromAll(locator) {
|
|
1939
2086
|
const els = await this._locate(locator);
|
|
1940
2087
|
this.debug(`Matched ${els.length} elements`);
|
|
1941
|
-
return Promise.all(els.map(el => el
|
|
2088
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
1942
2089
|
}
|
|
1943
2090
|
|
|
1944
2091
|
/**
|
|
@@ -1959,7 +2106,7 @@ class Playwright extends Helper {
|
|
|
1959
2106
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1960
2107
|
const els = await this._locate(locator);
|
|
1961
2108
|
this.debug(`Matched ${els.length} elements`);
|
|
1962
|
-
const cssValues = await Promise.all(els.map(el => el
|
|
2109
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
1963
2110
|
|
|
1964
2111
|
return cssValues;
|
|
1965
2112
|
}
|
|
@@ -1974,28 +2121,26 @@ class Playwright extends Helper {
|
|
|
1974
2121
|
|
|
1975
2122
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
1976
2123
|
const elemAmount = res.length;
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
});
|
|
1991
|
-
});
|
|
1992
|
-
let props = await Promise.all(commands);
|
|
2124
|
+
let props = [];
|
|
2125
|
+
|
|
2126
|
+
for (const element of res) {
|
|
2127
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
2128
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
2129
|
+
if (isColorProperty(prop)) {
|
|
2130
|
+
props.push(convertColorToRGBA(cssProp));
|
|
2131
|
+
} else {
|
|
2132
|
+
props.push(cssProp);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1993
2137
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
1994
2138
|
if (!Array.isArray(props)) props = [props];
|
|
1995
2139
|
let chunked = chunkArray(props, values.length);
|
|
1996
2140
|
chunked = chunked.filter((val) => {
|
|
1997
2141
|
for (let i = 0; i < val.length; ++i) {
|
|
1998
|
-
|
|
2142
|
+
// eslint-disable-next-line eqeqeq
|
|
2143
|
+
if (val[i] != values[i]) return false;
|
|
1999
2144
|
}
|
|
2000
2145
|
return true;
|
|
2001
2146
|
});
|
|
@@ -2015,7 +2160,7 @@ class Playwright extends Helper {
|
|
|
2015
2160
|
res.forEach((el) => {
|
|
2016
2161
|
Object.keys(attributes).forEach((prop) => {
|
|
2017
2162
|
commands.push(el
|
|
2018
|
-
|
|
2163
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
2019
2164
|
});
|
|
2020
2165
|
});
|
|
2021
2166
|
let attrs = await Promise.all(commands);
|
|
@@ -2024,7 +2169,8 @@ class Playwright extends Helper {
|
|
|
2024
2169
|
let chunked = chunkArray(attrs, values.length);
|
|
2025
2170
|
chunked = chunked.filter((val) => {
|
|
2026
2171
|
for (let i = 0; i < val.length; ++i) {
|
|
2027
|
-
if
|
|
2172
|
+
// if the attribute doesn't exist, returns false as well
|
|
2173
|
+
if (!val[i] || !val[i].includes(values[i])) return false;
|
|
2028
2174
|
}
|
|
2029
2175
|
return true;
|
|
2030
2176
|
});
|
|
@@ -2036,11 +2182,11 @@ class Playwright extends Helper {
|
|
|
2036
2182
|
*
|
|
2037
2183
|
*/
|
|
2038
2184
|
async dragSlider(locator, offsetX = 0) {
|
|
2039
|
-
const src = await this.
|
|
2185
|
+
const src = await this._locateElement(locator);
|
|
2040
2186
|
assertElementExists(src, locator, 'Slider Element');
|
|
2041
2187
|
|
|
2042
2188
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
2043
|
-
const sliderSource = await clickablePoint(src
|
|
2189
|
+
const sliderSource = await clickablePoint(src);
|
|
2044
2190
|
|
|
2045
2191
|
// Drag start point
|
|
2046
2192
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2074,8 +2220,7 @@ class Playwright extends Helper {
|
|
|
2074
2220
|
const array = [];
|
|
2075
2221
|
|
|
2076
2222
|
for (let index = 0; index < els.length; index++) {
|
|
2077
|
-
|
|
2078
|
-
array.push(await a.jsonValue());
|
|
2223
|
+
array.push(await els[index].getAttribute(attr));
|
|
2079
2224
|
}
|
|
2080
2225
|
|
|
2081
2226
|
return array;
|
|
@@ -2088,10 +2233,9 @@ class Playwright extends Helper {
|
|
|
2088
2233
|
async saveElementScreenshot(locator, fileName) {
|
|
2089
2234
|
const outputFile = screenshotOutputFolder(fileName);
|
|
2090
2235
|
|
|
2091
|
-
const res = await this.
|
|
2236
|
+
const res = await this._locateElement(locator);
|
|
2092
2237
|
assertElementExists(res, locator);
|
|
2093
|
-
|
|
2094
|
-
const elem = res[0];
|
|
2238
|
+
const elem = res;
|
|
2095
2239
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
2096
2240
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
2097
2241
|
}
|
|
@@ -2197,6 +2341,10 @@ class Playwright extends Helper {
|
|
|
2197
2341
|
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
2198
2342
|
}
|
|
2199
2343
|
}
|
|
2344
|
+
|
|
2345
|
+
if (this.options.recordHar) {
|
|
2346
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2347
|
+
}
|
|
2200
2348
|
}
|
|
2201
2349
|
|
|
2202
2350
|
async _passed(test) {
|
|
@@ -2224,6 +2372,10 @@ class Playwright extends Helper {
|
|
|
2224
2372
|
await this.browserContext.tracing.stop();
|
|
2225
2373
|
}
|
|
2226
2374
|
}
|
|
2375
|
+
|
|
2376
|
+
if (this.options.recordHar) {
|
|
2377
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2378
|
+
}
|
|
2227
2379
|
}
|
|
2228
2380
|
|
|
2229
2381
|
/**
|
|
@@ -2336,25 +2488,42 @@ class Playwright extends Helper {
|
|
|
2336
2488
|
locator = new Locator(locator, 'css');
|
|
2337
2489
|
|
|
2338
2490
|
const context = await this._getContext();
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2491
|
+
try {
|
|
2492
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' });
|
|
2493
|
+
} catch (e) {
|
|
2494
|
+
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2495
|
+
}
|
|
2343
2496
|
}
|
|
2344
2497
|
|
|
2345
2498
|
/**
|
|
2346
|
-
* {{> waitForVisible }}
|
|
2347
|
-
*
|
|
2348
2499
|
* This method accepts [React selectors](https://codecept.io/react).
|
|
2500
|
+
*
|
|
2501
|
+
* {{> waitForVisible }}
|
|
2349
2502
|
*/
|
|
2350
2503
|
async waitForVisible(locator, sec) {
|
|
2351
2504
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2352
2505
|
locator = new Locator(locator, 'css');
|
|
2353
2506
|
const context = await this._getContext();
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2507
|
+
let count = 0;
|
|
2508
|
+
|
|
2509
|
+
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2510
|
+
let waiter;
|
|
2511
|
+
if (this.frame) {
|
|
2512
|
+
do {
|
|
2513
|
+
waiter = await this.frame.locator(buildLocatorString(locator)).first().isVisible();
|
|
2514
|
+
await this.wait(1);
|
|
2515
|
+
count += 1000;
|
|
2516
|
+
if (waiter) break;
|
|
2517
|
+
} while (count <= waitTimeout);
|
|
2518
|
+
|
|
2519
|
+
if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
try {
|
|
2523
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'visible' });
|
|
2524
|
+
} catch (e) {
|
|
2525
|
+
throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2526
|
+
}
|
|
2358
2527
|
}
|
|
2359
2528
|
|
|
2360
2529
|
/**
|
|
@@ -2364,10 +2533,27 @@ class Playwright extends Helper {
|
|
|
2364
2533
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2365
2534
|
locator = new Locator(locator, 'css');
|
|
2366
2535
|
const context = await this._getContext();
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2536
|
+
let waiter;
|
|
2537
|
+
let count = 0;
|
|
2538
|
+
|
|
2539
|
+
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2540
|
+
if (this.frame) {
|
|
2541
|
+
do {
|
|
2542
|
+
waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden();
|
|
2543
|
+
await this.wait(1);
|
|
2544
|
+
count += 1000;
|
|
2545
|
+
if (waiter) break;
|
|
2546
|
+
} while (count <= waitTimeout);
|
|
2547
|
+
|
|
2548
|
+
if (!waiter) throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec.`);
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
try {
|
|
2553
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'hidden' });
|
|
2554
|
+
} catch (e) {
|
|
2555
|
+
throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2556
|
+
}
|
|
2371
2557
|
}
|
|
2372
2558
|
|
|
2373
2559
|
/**
|
|
@@ -2377,13 +2563,47 @@ class Playwright extends Helper {
|
|
|
2377
2563
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2378
2564
|
locator = new Locator(locator, 'css');
|
|
2379
2565
|
const context = await this._getContext();
|
|
2380
|
-
|
|
2566
|
+
let waiter;
|
|
2567
|
+
let count = 0;
|
|
2568
|
+
|
|
2569
|
+
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2570
|
+
if (this.frame) {
|
|
2571
|
+
do {
|
|
2572
|
+
waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden();
|
|
2573
|
+
await this.wait(1);
|
|
2574
|
+
count += 1000;
|
|
2575
|
+
if (waiter) break;
|
|
2576
|
+
} while (count <= waitTimeout);
|
|
2577
|
+
|
|
2578
|
+
if (!waiter) throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec.`);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
return context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'hidden' }).catch((err) => {
|
|
2381
2583
|
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2382
2584
|
});
|
|
2383
2585
|
}
|
|
2384
2586
|
|
|
2587
|
+
/**
|
|
2588
|
+
* {{> waitForNumberOfTabs }}
|
|
2589
|
+
*/
|
|
2590
|
+
async waitForNumberOfTabs(expectedTabs, sec) {
|
|
2591
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2592
|
+
let currentTabs;
|
|
2593
|
+
let count = 0;
|
|
2594
|
+
|
|
2595
|
+
do {
|
|
2596
|
+
currentTabs = await this.grabNumberOfOpenTabs();
|
|
2597
|
+
await this.wait(1);
|
|
2598
|
+
count += 1000;
|
|
2599
|
+
if (currentTabs >= expectedTabs) return;
|
|
2600
|
+
} while (count <= waitTimeout);
|
|
2601
|
+
|
|
2602
|
+
throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2385
2605
|
async _getContext() {
|
|
2386
|
-
if (this.context && this.context.constructor.name === '
|
|
2606
|
+
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2387
2607
|
return this.context;
|
|
2388
2608
|
}
|
|
2389
2609
|
return this.page;
|
|
@@ -2444,7 +2664,12 @@ class Playwright extends Helper {
|
|
|
2444
2664
|
if (context) {
|
|
2445
2665
|
const locator = new Locator(context, 'css');
|
|
2446
2666
|
if (!locator.isXPath()) {
|
|
2447
|
-
|
|
2667
|
+
try {
|
|
2668
|
+
await contextObject.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' });
|
|
2669
|
+
} catch (e) {
|
|
2670
|
+
console.log(e);
|
|
2671
|
+
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2672
|
+
}
|
|
2448
2673
|
}
|
|
2449
2674
|
|
|
2450
2675
|
if (locator.isXPath()) {
|
|
@@ -2456,11 +2681,19 @@ class Playwright extends Helper {
|
|
|
2456
2681
|
}, [locator.value, text, $XPath.toString()], { timeout: waitTimeout });
|
|
2457
2682
|
}
|
|
2458
2683
|
} else {
|
|
2459
|
-
|
|
2684
|
+
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2685
|
+
// eslint-disable-next-line no-lonely-if
|
|
2686
|
+
const _contextObject = this.frame ? this.frame : contextObject;
|
|
2687
|
+
let count = 0;
|
|
2688
|
+
do {
|
|
2689
|
+
waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
|
|
2690
|
+
if (waiter) break;
|
|
2691
|
+
await this.wait(1);
|
|
2692
|
+
count += 1000;
|
|
2693
|
+
} while (count <= waitTimeout);
|
|
2694
|
+
|
|
2695
|
+
if (!waiter) throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec`);
|
|
2460
2696
|
}
|
|
2461
|
-
return waiter.catch((err) => {
|
|
2462
|
-
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2463
|
-
});
|
|
2464
2697
|
}
|
|
2465
2698
|
|
|
2466
2699
|
/**
|
|
@@ -2510,29 +2743,42 @@ class Playwright extends Helper {
|
|
|
2510
2743
|
}
|
|
2511
2744
|
|
|
2512
2745
|
if (locator >= 0 && locator < childFrames.length) {
|
|
2513
|
-
this.context =
|
|
2746
|
+
this.context = await this.page.frameLocator('iframe').nth(locator);
|
|
2514
2747
|
this.contextLocator = locator;
|
|
2515
2748
|
} else {
|
|
2516
2749
|
throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath');
|
|
2517
2750
|
}
|
|
2518
2751
|
return;
|
|
2519
2752
|
}
|
|
2753
|
+
|
|
2520
2754
|
if (!locator) {
|
|
2521
2755
|
this.context = this.page;
|
|
2522
2756
|
this.contextLocator = null;
|
|
2757
|
+
this.frame = null;
|
|
2523
2758
|
return;
|
|
2524
2759
|
}
|
|
2525
2760
|
|
|
2526
2761
|
// iframe by selector
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2762
|
+
locator = buildLocatorString(new Locator(locator, 'css'));
|
|
2763
|
+
const frame = await this._locateElement(locator);
|
|
2764
|
+
|
|
2765
|
+
if (!frame) {
|
|
2766
|
+
throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
if (this.frame) {
|
|
2770
|
+
this.frame = await this.frame.frameLocator(locator);
|
|
2771
|
+
} else {
|
|
2772
|
+
this.frame = await this.page.frameLocator(locator);
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
const contentFrame = this.frame;
|
|
2530
2776
|
|
|
2531
2777
|
if (contentFrame) {
|
|
2532
2778
|
this.context = contentFrame;
|
|
2533
2779
|
this.contextLocator = null;
|
|
2534
2780
|
} else {
|
|
2535
|
-
this.context =
|
|
2781
|
+
this.context = this.page.frame(this.page.frames()[1].name());
|
|
2536
2782
|
this.contextLocator = locator;
|
|
2537
2783
|
}
|
|
2538
2784
|
}
|
|
@@ -2555,13 +2801,15 @@ class Playwright extends Helper {
|
|
|
2555
2801
|
}
|
|
2556
2802
|
|
|
2557
2803
|
/**
|
|
2558
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
2804
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
2559
2805
|
*
|
|
2560
2806
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2561
2807
|
*
|
|
2562
2808
|
* @param {*} options
|
|
2563
2809
|
*/
|
|
2564
2810
|
async waitForNavigation(options = {}) {
|
|
2811
|
+
console.log(`waitForNavigation deprecated:
|
|
2812
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
2565
2813
|
options = {
|
|
2566
2814
|
timeout: this.options.getPageTimeout,
|
|
2567
2815
|
waitUntil: this.options.waitForNavigation,
|
|
@@ -2570,6 +2818,23 @@ class Playwright extends Helper {
|
|
|
2570
2818
|
return this.page.waitForNavigation(options);
|
|
2571
2819
|
}
|
|
2572
2820
|
|
|
2821
|
+
/**
|
|
2822
|
+
* Waits for page navigates to a new URL or reloads. By default, it takes configured `waitForNavigation` option.
|
|
2823
|
+
*
|
|
2824
|
+
* See [Playwright's reference](https://playwright.dev/docs/api/class-page#page-wait-for-url)
|
|
2825
|
+
*
|
|
2826
|
+
* @param {string|RegExp} url - A glob pattern, regex pattern or predicate receiving URL to match while waiting for the navigation. Note that if the parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to the string.
|
|
2827
|
+
* @param {*} options
|
|
2828
|
+
*/
|
|
2829
|
+
async waitForURL(url, options = {}) {
|
|
2830
|
+
options = {
|
|
2831
|
+
timeout: this.options.getPageTimeout,
|
|
2832
|
+
waitUntil: this.options.waitForNavigation,
|
|
2833
|
+
...options,
|
|
2834
|
+
};
|
|
2835
|
+
return this.page.waitForURL(url, options);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2573
2838
|
async waitUntilExists(locator, sec) {
|
|
2574
2839
|
console.log(`waitUntilExists deprecated:
|
|
2575
2840
|
* use 'waitForElement' to wait for element to be attached
|
|
@@ -2587,17 +2852,21 @@ class Playwright extends Helper {
|
|
|
2587
2852
|
let waiter;
|
|
2588
2853
|
const context = await this._getContext();
|
|
2589
2854
|
if (!locator.isXPath()) {
|
|
2590
|
-
|
|
2855
|
+
try {
|
|
2856
|
+
await context.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`).first().waitFor({ timeout: waitTimeout, state: 'detached' });
|
|
2857
|
+
} catch (e) {
|
|
2858
|
+
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${e.message}`);
|
|
2859
|
+
}
|
|
2591
2860
|
} else {
|
|
2592
2861
|
const visibleFn = function ([locator, $XPath]) {
|
|
2593
2862
|
eval($XPath); // eslint-disable-line no-eval
|
|
2594
2863
|
return $XPath(null, locator).length === 0;
|
|
2595
2864
|
};
|
|
2596
2865
|
waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout });
|
|
2866
|
+
return waiter.catch((err) => {
|
|
2867
|
+
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2868
|
+
});
|
|
2597
2869
|
}
|
|
2598
|
-
return waiter.catch((err) => {
|
|
2599
|
-
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2600
|
-
});
|
|
2601
2870
|
}
|
|
2602
2871
|
|
|
2603
2872
|
async _waitForAction() {
|
|
@@ -2615,9 +2884,9 @@ class Playwright extends Helper {
|
|
|
2615
2884
|
* {{> grabElementBoundingRect }}
|
|
2616
2885
|
*/
|
|
2617
2886
|
async grabElementBoundingRect(locator, prop) {
|
|
2618
|
-
const
|
|
2619
|
-
assertElementExists(
|
|
2620
|
-
const rect = await
|
|
2887
|
+
const el = await this._locateElement(locator);
|
|
2888
|
+
assertElementExists(el, locator);
|
|
2889
|
+
const rect = await el.boundingBox();
|
|
2621
2890
|
if (prop) return rect[prop];
|
|
2622
2891
|
return rect;
|
|
2623
2892
|
}
|
|
@@ -2652,54 +2921,559 @@ class Playwright extends Helper {
|
|
|
2652
2921
|
async stopMockingRoute(url, handler) {
|
|
2653
2922
|
return this.browserContext.unroute(...arguments);
|
|
2654
2923
|
}
|
|
2655
|
-
}
|
|
2656
2924
|
|
|
2657
|
-
|
|
2925
|
+
/**
|
|
2926
|
+
* Starts recording the network traffics.
|
|
2927
|
+
* This also resets recorded network requests.
|
|
2928
|
+
*
|
|
2929
|
+
* ```js
|
|
2930
|
+
* I.startRecordingTraffic();
|
|
2931
|
+
* ```
|
|
2932
|
+
*
|
|
2933
|
+
* @return {void}
|
|
2934
|
+
*/
|
|
2935
|
+
startRecordingTraffic() {
|
|
2936
|
+
this.flushNetworkTraffics();
|
|
2937
|
+
this.recording = true;
|
|
2938
|
+
this.recordedAtLeastOnce = true;
|
|
2658
2939
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
while (sibling) {
|
|
2673
|
-
if (sibling.tagName === node.tagName) {
|
|
2674
|
-
index++;
|
|
2940
|
+
this.page.on('requestfinished', async (request) => {
|
|
2941
|
+
const information = {
|
|
2942
|
+
url: request.url(),
|
|
2943
|
+
method: request.method(),
|
|
2944
|
+
requestHeaders: request.headers(),
|
|
2945
|
+
requestPostData: request.postData(),
|
|
2946
|
+
response: request.response(),
|
|
2947
|
+
};
|
|
2948
|
+
|
|
2949
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2950
|
+
|
|
2951
|
+
if (typeof information.requestPostData === 'object') {
|
|
2952
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2675
2953
|
}
|
|
2676
|
-
|
|
2954
|
+
|
|
2955
|
+
this.requests.push(information);
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* Grab the recording network traffics
|
|
2961
|
+
*
|
|
2962
|
+
* ```js
|
|
2963
|
+
* const traffics = await I.grabRecordedNetworkTraffics();
|
|
2964
|
+
* expect(traffics[0].url).to.equal('https://reqres.in/api/comments/1');
|
|
2965
|
+
* expect(traffics[0].response.status).to.equal(200);
|
|
2966
|
+
* expect(traffics[0].response.body).to.contain({ name: 'this was mocked' });
|
|
2967
|
+
* ```
|
|
2968
|
+
*
|
|
2969
|
+
* @return { Promise<Array<any>> }
|
|
2970
|
+
*
|
|
2971
|
+
*/
|
|
2972
|
+
async grabRecordedNetworkTraffics() {
|
|
2973
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2974
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
const promises = this.requests.map(async (request) => {
|
|
2978
|
+
const resp = await request.response;
|
|
2979
|
+
let body;
|
|
2980
|
+
try {
|
|
2981
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
2982
|
+
body = JSON.parse((await resp.body()).toString());
|
|
2983
|
+
} catch (e) {
|
|
2984
|
+
// only interested in JSON, not HTML responses.
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
return {
|
|
2988
|
+
url: resp.url(),
|
|
2989
|
+
response: {
|
|
2990
|
+
status: resp.status(),
|
|
2991
|
+
statusText: resp.statusText(),
|
|
2992
|
+
body,
|
|
2993
|
+
},
|
|
2994
|
+
};
|
|
2995
|
+
});
|
|
2996
|
+
|
|
2997
|
+
return Promise.all(promises);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
/**
|
|
3001
|
+
* Blocks traffic of a given URL or a list of URLs.
|
|
3002
|
+
*
|
|
3003
|
+
* Examples:
|
|
3004
|
+
*
|
|
3005
|
+
* ```js
|
|
3006
|
+
* I.blockTraffic('http://example.com/css/style.css');
|
|
3007
|
+
* I.blockTraffic('http://example.com/css/*.css');
|
|
3008
|
+
* I.blockTraffic('http://example.com/**');
|
|
3009
|
+
* I.blockTraffic(/\.css$/);
|
|
3010
|
+
* ```
|
|
3011
|
+
*
|
|
3012
|
+
* ```js
|
|
3013
|
+
* I.blockTraffic(['http://example.com/css/style.css', 'http://example.com/css/*.css']);
|
|
3014
|
+
* ```
|
|
3015
|
+
*
|
|
3016
|
+
* @param {string|Array|RegExp} urls URL or a list of URLs to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
|
|
3017
|
+
*/
|
|
3018
|
+
blockTraffic(urls) {
|
|
3019
|
+
if (Array.isArray(urls)) {
|
|
3020
|
+
urls.forEach(url => {
|
|
3021
|
+
this.page.route(url, (route) => {
|
|
3022
|
+
route
|
|
3023
|
+
.abort()
|
|
3024
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3025
|
+
.catch((e) => {});
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
} else {
|
|
3029
|
+
this.page.route(urls, (route) => {
|
|
3030
|
+
route
|
|
3031
|
+
.abort()
|
|
3032
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
3033
|
+
.catch((e) => {});
|
|
3034
|
+
});
|
|
2677
3035
|
}
|
|
2678
|
-
return index;
|
|
2679
3036
|
}
|
|
2680
3037
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
3038
|
+
/**
|
|
3039
|
+
* Mocks traffic for URL(s).
|
|
3040
|
+
* This is a powerful feature to manipulate network traffic. Can be used e.g. to stabilize your tests, speed up your tests or as a last resort to make some test scenarios even possible.
|
|
3041
|
+
*
|
|
3042
|
+
* Examples:
|
|
3043
|
+
*
|
|
3044
|
+
* ```js
|
|
3045
|
+
* I.mockTraffic('/api/users/1', '{ id: 1, name: 'John Doe' }');
|
|
3046
|
+
* I.mockTraffic('/api/users/*', JSON.stringify({ id: 1, name: 'John Doe' }));
|
|
3047
|
+
* I.mockTraffic([/^https://api.example.com/v1/, 'https://api.example.com/v2/**'], 'Internal Server Error', 'text/html');
|
|
3048
|
+
* ```
|
|
3049
|
+
*
|
|
3050
|
+
* @param urls string|Array These are the URL(s) to mock, e.g. "/fooapi/*" or "['/fooapi_1/*', '/barapi_2/*']". Regular expressions are also supported.
|
|
3051
|
+
* @param responseString string The string to return in fake response's body.
|
|
3052
|
+
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
3053
|
+
*/
|
|
3054
|
+
mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
3055
|
+
// Required to mock cross-domain requests
|
|
3056
|
+
const headers = { 'access-control-allow-origin': '*' };
|
|
3057
|
+
|
|
3058
|
+
if (typeof urls === 'string') {
|
|
3059
|
+
urls = [urls];
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
urls.forEach((url) => {
|
|
3063
|
+
this.page.route(url, (route) => {
|
|
3064
|
+
if (this.page.isClosed()) {
|
|
3065
|
+
// Sometimes it happens that browser has been closed in the meantime.
|
|
3066
|
+
// In this case we just don't fulfill to prevent error in test scenario.
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
route.fulfill({
|
|
3070
|
+
contentType,
|
|
3071
|
+
headers,
|
|
3072
|
+
body: responseString,
|
|
3073
|
+
});
|
|
3074
|
+
});
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
/**
|
|
3079
|
+
* Resets all recorded network requests.
|
|
3080
|
+
*/
|
|
3081
|
+
flushNetworkTraffics() {
|
|
3082
|
+
this.requests = [];
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* Stops recording of network traffic. Recorded traffic is not flashed.
|
|
3087
|
+
*
|
|
3088
|
+
* ```js
|
|
3089
|
+
* I.stopRecordingTraffic();
|
|
3090
|
+
* ```
|
|
3091
|
+
*/
|
|
3092
|
+
stopRecordingTraffic() {
|
|
3093
|
+
this.page.removeAllListeners('request');
|
|
3094
|
+
this.recording = false;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* Verifies that a certain request is part of network traffic.
|
|
3099
|
+
*
|
|
3100
|
+
* ```js
|
|
3101
|
+
* // checking the request url contains certain query strings
|
|
3102
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3103
|
+
* I.startRecordingTraffic();
|
|
3104
|
+
* await I.seeTraffic({
|
|
3105
|
+
* name: 'sentry event',
|
|
3106
|
+
* url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
3107
|
+
* parameters: {
|
|
3108
|
+
* width: '1919',
|
|
3109
|
+
* height: '1138',
|
|
3110
|
+
* },
|
|
3111
|
+
* });
|
|
3112
|
+
* ```
|
|
3113
|
+
*
|
|
3114
|
+
* ```js
|
|
3115
|
+
* // checking the request url contains certain post data
|
|
3116
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
3117
|
+
* I.startRecordingTraffic();
|
|
3118
|
+
* await I.seeTraffic({
|
|
3119
|
+
* name: 'event',
|
|
3120
|
+
* url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
3121
|
+
* requestPostData: {
|
|
3122
|
+
* st: 2,
|
|
3123
|
+
* },
|
|
3124
|
+
* });
|
|
3125
|
+
* ```
|
|
3126
|
+
*
|
|
3127
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
3128
|
+
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
3129
|
+
* @param {string} opts.url Expected URL of request in network traffic
|
|
3130
|
+
* @param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
3131
|
+
* @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
3132
|
+
* @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
3133
|
+
* @return { Promise<*> }
|
|
3134
|
+
*/
|
|
3135
|
+
async seeTraffic({
|
|
3136
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
3137
|
+
}) {
|
|
3138
|
+
if (!name) {
|
|
3139
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
if (!url) {
|
|
3143
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3147
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
3151
|
+
const found = this._isInTraffic(url, parameters);
|
|
3152
|
+
if (found) {
|
|
3153
|
+
return true;
|
|
3154
|
+
}
|
|
3155
|
+
await new Promise((done) => {
|
|
3156
|
+
setTimeout(done, 1000);
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// check request post data
|
|
3161
|
+
if (requestPostData && this._isInTraffic(url)) {
|
|
3162
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
3163
|
+
|
|
3164
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
3165
|
+
} else if (parameters && this._isInTraffic(url)) {
|
|
3166
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
3167
|
+
|
|
3168
|
+
assert.fail(
|
|
3169
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
3170
|
+
+ `${advancedTestResults}`,
|
|
3171
|
+
);
|
|
3172
|
+
} else {
|
|
3173
|
+
assert.fail(
|
|
3174
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
3175
|
+
+ `Expected url: ${url}.\n`
|
|
3176
|
+
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
/**
|
|
3182
|
+
* Returns full URL of request matching parameter "urlMatch".
|
|
3183
|
+
*
|
|
3184
|
+
* @param {string|RegExp} urlMatch Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3185
|
+
*
|
|
3186
|
+
* Examples:
|
|
3187
|
+
*
|
|
3188
|
+
* ```js
|
|
3189
|
+
* I.grabTrafficUrl('https://api.example.com/session');
|
|
3190
|
+
* I.grabTrafficUrl(/session.*start/);
|
|
3191
|
+
* ```
|
|
3192
|
+
*
|
|
3193
|
+
* @return {Promise<*>}
|
|
3194
|
+
*/
|
|
3195
|
+
grabTrafficUrl(urlMatch) {
|
|
3196
|
+
if (!this.recordedAtLeastOnce) {
|
|
3197
|
+
throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.');
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
for (const i in this.requests) {
|
|
3201
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
3202
|
+
if (this.requests.hasOwnProperty(i)) {
|
|
3203
|
+
const request = this.requests[i];
|
|
3204
|
+
|
|
3205
|
+
if (request.url && request.url.match(new RegExp(urlMatch))) {
|
|
3206
|
+
return request.url;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* Verifies that a certain request is not part of network traffic.
|
|
3216
|
+
*
|
|
3217
|
+
* Examples:
|
|
3218
|
+
*
|
|
3219
|
+
* ```js
|
|
3220
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
3221
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
3222
|
+
* ```
|
|
3223
|
+
*
|
|
3224
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
3225
|
+
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
3226
|
+
* @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
3227
|
+
*
|
|
3228
|
+
*/
|
|
3229
|
+
dontSeeTraffic({ name, url }) {
|
|
3230
|
+
if (!this.recordedAtLeastOnce) {
|
|
3231
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
if (!name) {
|
|
3235
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
if (!url) {
|
|
3239
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
if (this._isInTraffic(url)) {
|
|
3243
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
/**
|
|
3248
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
3249
|
+
*
|
|
3250
|
+
* @param url URL to look for.
|
|
3251
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
3252
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
3253
|
+
* @private
|
|
3254
|
+
*/
|
|
3255
|
+
_isInTraffic(url, parameters) {
|
|
3256
|
+
let isInTraffic = false;
|
|
3257
|
+
this.requests.forEach((request) => {
|
|
3258
|
+
if (isInTraffic) {
|
|
3259
|
+
return; // We already found traffic. Continue with next request
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
if (!request.url.match(new RegExp(url))) {
|
|
3263
|
+
return; // url not found in this request. continue with next request
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// URL has matched. Now we check the parameters
|
|
3267
|
+
|
|
3268
|
+
if (parameters) {
|
|
3269
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
3270
|
+
if (advancedReport === true) {
|
|
3271
|
+
isInTraffic = true;
|
|
3272
|
+
}
|
|
2687
3273
|
} else {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3274
|
+
isInTraffic = true;
|
|
3275
|
+
}
|
|
3276
|
+
});
|
|
3277
|
+
|
|
3278
|
+
return isInTraffic;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
/**
|
|
3282
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
3283
|
+
*
|
|
3284
|
+
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
|
|
3285
|
+
* @private
|
|
3286
|
+
*/
|
|
3287
|
+
_getTrafficDump() {
|
|
3288
|
+
let dumpedTraffic = '';
|
|
3289
|
+
this.requests.forEach((request) => {
|
|
3290
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
3291
|
+
});
|
|
3292
|
+
return dumpedTraffic;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
/**
|
|
3296
|
+
* Starts recording of websocket messages.
|
|
3297
|
+
* This also resets recorded websocket messages.
|
|
3298
|
+
*
|
|
3299
|
+
* ```js
|
|
3300
|
+
* await I.startRecordingWebSocketMessages();
|
|
3301
|
+
* ```
|
|
3302
|
+
*
|
|
3303
|
+
*/
|
|
3304
|
+
async startRecordingWebSocketMessages() {
|
|
3305
|
+
this.flushWebSocketMessages();
|
|
3306
|
+
this.recordingWebSocketMessages = true;
|
|
3307
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
3308
|
+
|
|
3309
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
3310
|
+
await this.cdpSession.send('Network.enable');
|
|
3311
|
+
await this.cdpSession.send('Page.enable');
|
|
3312
|
+
|
|
3313
|
+
this.cdpSession.on(
|
|
3314
|
+
'Network.webSocketFrameReceived',
|
|
3315
|
+
(payload) => {
|
|
3316
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
3317
|
+
},
|
|
3318
|
+
);
|
|
3319
|
+
|
|
3320
|
+
this.cdpSession.on(
|
|
3321
|
+
'Network.webSocketFrameSent',
|
|
3322
|
+
(payload) => {
|
|
3323
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
3324
|
+
},
|
|
3325
|
+
);
|
|
3326
|
+
|
|
3327
|
+
this.cdpSession.on(
|
|
3328
|
+
'Network.webSocketFrameError',
|
|
3329
|
+
(payload) => {
|
|
3330
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
3331
|
+
},
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
/**
|
|
3336
|
+
* Stops recording WS messages. Recorded WS messages is not flashed.
|
|
3337
|
+
*
|
|
3338
|
+
* ```js
|
|
3339
|
+
* await I.stopRecordingWebSocketMessages();
|
|
3340
|
+
* ```
|
|
3341
|
+
*/
|
|
3342
|
+
async stopRecordingWebSocketMessages() {
|
|
3343
|
+
await this.cdpSession.send('Network.disable');
|
|
3344
|
+
await this.cdpSession.send('Page.disable');
|
|
3345
|
+
this.page.removeAllListeners('Network');
|
|
3346
|
+
this.recordingWebSocketMessages = false;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
/**
|
|
3350
|
+
* Grab the recording WS messages
|
|
3351
|
+
*
|
|
3352
|
+
* @return { Array<any> }
|
|
3353
|
+
*
|
|
3354
|
+
*/
|
|
3355
|
+
grabWebSocketMessages() {
|
|
3356
|
+
if (!this.recordingWebSocketMessages) {
|
|
3357
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
3358
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
2691
3359
|
}
|
|
2692
3360
|
}
|
|
2693
|
-
return
|
|
3361
|
+
return this.webSocketMessages;
|
|
2694
3362
|
}
|
|
2695
3363
|
|
|
2696
|
-
|
|
3364
|
+
/**
|
|
3365
|
+
* Resets all recorded WS messages.
|
|
3366
|
+
*/
|
|
3367
|
+
flushWebSocketMessages() {
|
|
3368
|
+
this.webSocketMessages = [];
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
/**
|
|
3372
|
+
* Return a performance metric from the chrome cdp session.
|
|
3373
|
+
* Note: Chrome-only
|
|
3374
|
+
*
|
|
3375
|
+
* Examples:
|
|
3376
|
+
*
|
|
3377
|
+
* ```js
|
|
3378
|
+
* const metrics = await I.grabMetrics();
|
|
3379
|
+
*
|
|
3380
|
+
* // returned metrics
|
|
3381
|
+
*
|
|
3382
|
+
* [
|
|
3383
|
+
* { name: 'Timestamp', value: 1584904.203473 },
|
|
3384
|
+
* { name: 'AudioHandlers', value: 0 },
|
|
3385
|
+
* { name: 'AudioWorkletProcessors', value: 0 },
|
|
3386
|
+
* { name: 'Documents', value: 22 },
|
|
3387
|
+
* { name: 'Frames', value: 10 },
|
|
3388
|
+
* { name: 'JSEventListeners', value: 366 },
|
|
3389
|
+
* { name: 'LayoutObjects', value: 1240 },
|
|
3390
|
+
* { name: 'MediaKeySessions', value: 0 },
|
|
3391
|
+
* { name: 'MediaKeys', value: 0 },
|
|
3392
|
+
* { name: 'Nodes', value: 4505 },
|
|
3393
|
+
* { name: 'Resources', value: 141 },
|
|
3394
|
+
* { name: 'ContextLifecycleStateObservers', value: 34 },
|
|
3395
|
+
* { name: 'V8PerContextDatas', value: 4 },
|
|
3396
|
+
* { name: 'WorkerGlobalScopes', value: 0 },
|
|
3397
|
+
* { name: 'UACSSResources', value: 0 },
|
|
3398
|
+
* { name: 'RTCPeerConnections', value: 0 },
|
|
3399
|
+
* { name: 'ResourceFetchers', value: 22 },
|
|
3400
|
+
* { name: 'AdSubframes', value: 0 },
|
|
3401
|
+
* { name: 'DetachedScriptStates', value: 2 },
|
|
3402
|
+
* { name: 'ArrayBufferContents', value: 1 },
|
|
3403
|
+
* { name: 'LayoutCount', value: 0 },
|
|
3404
|
+
* { name: 'RecalcStyleCount', value: 0 },
|
|
3405
|
+
* { name: 'LayoutDuration', value: 0 },
|
|
3406
|
+
* { name: 'RecalcStyleDuration', value: 0 },
|
|
3407
|
+
* { name: 'DevToolsCommandDuration', value: 0.000013 },
|
|
3408
|
+
* { name: 'ScriptDuration', value: 0 },
|
|
3409
|
+
* { name: 'V8CompileDuration', value: 0 },
|
|
3410
|
+
* { name: 'TaskDuration', value: 0.000014 },
|
|
3411
|
+
* { name: 'TaskOtherDuration', value: 0.000001 },
|
|
3412
|
+
* { name: 'ThreadTime', value: 0.000046 },
|
|
3413
|
+
* { name: 'ProcessTime', value: 0.616852 },
|
|
3414
|
+
* { name: 'JSHeapUsedSize', value: 19004908 },
|
|
3415
|
+
* { name: 'JSHeapTotalSize', value: 26820608 },
|
|
3416
|
+
* { name: 'FirstMeaningfulPaint', value: 0 },
|
|
3417
|
+
* { name: 'DomContentLoaded', value: 1584903.690491 },
|
|
3418
|
+
* { name: 'NavigationStart', value: 1584902.841845 }
|
|
3419
|
+
* ]
|
|
3420
|
+
*
|
|
3421
|
+
* ```
|
|
3422
|
+
*
|
|
3423
|
+
* @return {Promise<Array<Object>>}
|
|
3424
|
+
*/
|
|
3425
|
+
async grabMetrics() {
|
|
3426
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
3427
|
+
await client.send('Performance.enable');
|
|
3428
|
+
const perfMetricObject = await client.send('Performance.getMetrics');
|
|
3429
|
+
return perfMetricObject?.metrics;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
_getWebSocketMessage(payload) {
|
|
3433
|
+
if (payload.errorMessage) {
|
|
3434
|
+
return payload.errorMessage;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
return payload.response.payloadData;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
_getWebSocketLog(prefix, payload) {
|
|
3441
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
async getNewCDPSession() {
|
|
3445
|
+
return this.page.context().newCDPSession(this.page);
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
_logWebsocketMessages(message) {
|
|
3449
|
+
this.webSocketMessages += message;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
module.exports = Playwright;
|
|
3454
|
+
|
|
3455
|
+
function buildLocatorString(locator) {
|
|
3456
|
+
if (locator.isCustom()) {
|
|
3457
|
+
return `${locator.type}=${locator.value}`;
|
|
3458
|
+
} if (locator.isXPath()) {
|
|
3459
|
+
return `xpath=${locator.value}`;
|
|
3460
|
+
}
|
|
3461
|
+
return locator.simplify();
|
|
2697
3462
|
}
|
|
2698
3463
|
|
|
2699
3464
|
async function findElements(matcher, locator) {
|
|
3465
|
+
if (locator.react) return findReact(matcher, locator);
|
|
3466
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
3467
|
+
locator = new Locator(locator, 'css');
|
|
3468
|
+
|
|
3469
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
async function findElement(matcher, locator) {
|
|
2700
3473
|
if (locator.react) return findReact(matcher, locator);
|
|
2701
3474
|
locator = new Locator(locator, 'css');
|
|
2702
|
-
|
|
3475
|
+
|
|
3476
|
+
return matcher.locator(buildLocatorString(locator)).first();
|
|
2703
3477
|
}
|
|
2704
3478
|
|
|
2705
3479
|
async function getVisibleElements(elements) {
|
|
@@ -2729,8 +3503,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2729
3503
|
assertElementExists(els, locator, 'Clickable element');
|
|
2730
3504
|
}
|
|
2731
3505
|
|
|
2732
|
-
|
|
2733
|
-
highlightActiveElement.call(this, els[0], this.page);
|
|
3506
|
+
await highlightActiveElement.call(this, els[0]);
|
|
2734
3507
|
|
|
2735
3508
|
/*
|
|
2736
3509
|
using the force true options itself but instead dispatching a click
|
|
@@ -2743,7 +3516,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2743
3516
|
}
|
|
2744
3517
|
const promises = [];
|
|
2745
3518
|
if (options.waitForNavigation) {
|
|
2746
|
-
promises.push(this.waitForNavigation
|
|
3519
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
2747
3520
|
}
|
|
2748
3521
|
promises.push(this._waitForAction());
|
|
2749
3522
|
|
|
@@ -2752,6 +3525,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2752
3525
|
|
|
2753
3526
|
async function findClickable(matcher, locator) {
|
|
2754
3527
|
if (locator.react) return findReact(matcher, locator);
|
|
3528
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
2755
3529
|
|
|
2756
3530
|
locator = new Locator(locator);
|
|
2757
3531
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
@@ -2778,28 +3552,25 @@ async function findClickable(matcher, locator) {
|
|
|
2778
3552
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
2779
3553
|
let description;
|
|
2780
3554
|
let allText;
|
|
3555
|
+
|
|
2781
3556
|
if (!context) {
|
|
2782
|
-
|
|
3557
|
+
const el = await this.context;
|
|
2783
3558
|
|
|
2784
|
-
|
|
2785
|
-
// Fallback to body
|
|
2786
|
-
el = await this.context.$('body');
|
|
2787
|
-
}
|
|
3559
|
+
allText = el.constructor.name !== 'Locator' ? [await el.locator('body').innerText()] : [await el.innerText()];
|
|
2788
3560
|
|
|
2789
|
-
allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
|
|
2790
3561
|
description = 'web application';
|
|
2791
3562
|
} else {
|
|
2792
3563
|
const locator = new Locator(context, 'css');
|
|
2793
3564
|
description = `element ${locator.toString()}`;
|
|
2794
3565
|
const els = await this._locate(locator);
|
|
2795
3566
|
assertElementExists(els, locator.toString());
|
|
2796
|
-
allText = await Promise.all(els.map(el => el.
|
|
3567
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
2797
3568
|
}
|
|
2798
3569
|
|
|
2799
3570
|
if (strict) {
|
|
2800
3571
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
2801
3572
|
}
|
|
2802
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
3573
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
2803
3574
|
}
|
|
2804
3575
|
|
|
2805
3576
|
async function findCheckable(locator, context) {
|
|
@@ -2861,15 +3632,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2861
3632
|
const els = await findFields.call(this, field);
|
|
2862
3633
|
assertElementExists(els, field, 'Field');
|
|
2863
3634
|
const el = els[0];
|
|
2864
|
-
const tag = await el.
|
|
2865
|
-
const fieldType = await el.
|
|
3635
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
3636
|
+
const fieldType = await el.getAttribute('type');
|
|
2866
3637
|
|
|
2867
3638
|
const proceedMultiple = async (elements) => {
|
|
2868
3639
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
2869
3640
|
|
|
2870
3641
|
const elementValues = [];
|
|
2871
3642
|
for (const element of fields) {
|
|
2872
|
-
elementValues.push(await element.
|
|
3643
|
+
elementValues.push(await element.inputValue());
|
|
2873
3644
|
}
|
|
2874
3645
|
|
|
2875
3646
|
if (typeof value === 'boolean') {
|
|
@@ -2883,8 +3654,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2883
3654
|
};
|
|
2884
3655
|
|
|
2885
3656
|
if (tag === 'SELECT') {
|
|
2886
|
-
if (await el.
|
|
2887
|
-
const selectedOptions = await el
|
|
3657
|
+
if (await el.getAttribute('multiple')) {
|
|
3658
|
+
const selectedOptions = await el.all('option:checked');
|
|
2888
3659
|
if (!selectedOptions.length) return null;
|
|
2889
3660
|
|
|
2890
3661
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -2908,14 +3679,23 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2908
3679
|
return proceedMultiple(els[0]);
|
|
2909
3680
|
}
|
|
2910
3681
|
|
|
2911
|
-
|
|
3682
|
+
let fieldVal;
|
|
3683
|
+
|
|
3684
|
+
try {
|
|
3685
|
+
fieldVal = await el.inputValue();
|
|
3686
|
+
} catch (e) {
|
|
3687
|
+
if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
|
|
3688
|
+
fieldVal = await el.innerText();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
|
|
2912
3692
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
2913
3693
|
}
|
|
2914
3694
|
|
|
2915
3695
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
2916
3696
|
const matches = [];
|
|
2917
3697
|
for (const element of elements) {
|
|
2918
|
-
const val = await element.
|
|
3698
|
+
const val = await element.getAttribute('value');
|
|
2919
3699
|
let isSelected = true;
|
|
2920
3700
|
if (onlySelected) {
|
|
2921
3701
|
isSelected = await elementSelected(element);
|
|
@@ -2939,17 +3719,19 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
2939
3719
|
}
|
|
2940
3720
|
|
|
2941
3721
|
async function elementSelected(element) {
|
|
2942
|
-
const type = await element.
|
|
3722
|
+
const type = await element.getAttribute('type');
|
|
2943
3723
|
|
|
2944
3724
|
if (type === 'checkbox' || type === 'radio') {
|
|
2945
3725
|
return element.isChecked();
|
|
2946
3726
|
}
|
|
2947
|
-
return element.
|
|
3727
|
+
return element.getAttribute('selected');
|
|
2948
3728
|
}
|
|
2949
3729
|
|
|
2950
3730
|
function isFrameLocator(locator) {
|
|
2951
3731
|
locator = new Locator(locator);
|
|
2952
|
-
if (locator.isFrame())
|
|
3732
|
+
if (locator.isFrame()) {
|
|
3733
|
+
return locator.value;
|
|
3734
|
+
}
|
|
2953
3735
|
return false;
|
|
2954
3736
|
}
|
|
2955
3737
|
|
|
@@ -3144,8 +3926,143 @@ async function saveTraceForContext(context, name) {
|
|
|
3144
3926
|
return fileName;
|
|
3145
3927
|
}
|
|
3146
3928
|
|
|
3147
|
-
function highlightActiveElement(element
|
|
3148
|
-
if (
|
|
3149
|
-
|
|
3150
|
-
|
|
3929
|
+
async function highlightActiveElement(element) {
|
|
3930
|
+
if (this.options.highlightElement && global.debugMode) {
|
|
3931
|
+
await element.evaluate(el => {
|
|
3932
|
+
const prevStyle = el.style.boxShadow;
|
|
3933
|
+
el.style.boxShadow = '0px 0px 4px 3px rgba(255, 0, 0, 0.7)';
|
|
3934
|
+
setTimeout(() => el.style.boxShadow = prevStyle, 2000);
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3151
3937
|
}
|
|
3938
|
+
|
|
3939
|
+
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
3940
|
+
// Creates advanced test results for a network traffic check.
|
|
3941
|
+
// Advanced test results only applies when expected parameters are set
|
|
3942
|
+
if (!dataToCheck) return '';
|
|
3943
|
+
|
|
3944
|
+
let urlFound = false;
|
|
3945
|
+
let advancedResults;
|
|
3946
|
+
requests.forEach((request) => {
|
|
3947
|
+
// url not found in this request. continue with next request
|
|
3948
|
+
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
3949
|
+
urlFound = true;
|
|
3950
|
+
|
|
3951
|
+
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
3952
|
+
if (!request.requestPostData) {
|
|
3953
|
+
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
3954
|
+
} else if (request.requestPostData) {
|
|
3955
|
+
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
3956
|
+
}
|
|
3957
|
+
});
|
|
3958
|
+
return advancedResults;
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
const extractQueryObjects = (queryString) => {
|
|
3962
|
+
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
3963
|
+
if (queryString.indexOf('?') === -1) {
|
|
3964
|
+
return [];
|
|
3965
|
+
}
|
|
3966
|
+
const queryObjects = [];
|
|
3967
|
+
|
|
3968
|
+
const queryPart = queryString.split('?')[1];
|
|
3969
|
+
|
|
3970
|
+
const queryParameters = queryPart.split('&');
|
|
3971
|
+
|
|
3972
|
+
queryParameters.forEach((queryParameter) => {
|
|
3973
|
+
const keyValue = queryParameter.split('=');
|
|
3974
|
+
const queryObject = {};
|
|
3975
|
+
// eslint-disable-next-line prefer-destructuring
|
|
3976
|
+
queryObject.name = keyValue[0];
|
|
3977
|
+
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
3978
|
+
queryObjects.push(queryObject);
|
|
3979
|
+
});
|
|
3980
|
+
|
|
3981
|
+
return queryObjects;
|
|
3982
|
+
};
|
|
3983
|
+
|
|
3984
|
+
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
3985
|
+
// More advanced check if all request parameters match with the expectations
|
|
3986
|
+
let littleReport = '\nQuery parameters:\n';
|
|
3987
|
+
let success = true;
|
|
3988
|
+
|
|
3989
|
+
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
3990
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
let parameterFound = false;
|
|
3994
|
+
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
3995
|
+
|
|
3996
|
+
for (const queryParameter of queryStringObject) {
|
|
3997
|
+
if (queryParameter.name === expectedKey) {
|
|
3998
|
+
parameterFound = true;
|
|
3999
|
+
if (expectedValue === undefined) {
|
|
4000
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4001
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4002
|
+
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
4003
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
4004
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4005
|
+
} else {
|
|
4006
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4007
|
+
success = false;
|
|
4008
|
+
}
|
|
4009
|
+
} else if (queryParameter.value === expectedValue) {
|
|
4010
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4011
|
+
} else {
|
|
4012
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
4013
|
+
success = false;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
if (parameterFound === false) {
|
|
4019
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
4020
|
+
success = false;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
return success ? true : littleReport;
|
|
4025
|
+
};
|
|
4026
|
+
|
|
4027
|
+
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
4028
|
+
// More advanced check if all request post data match with the expectations
|
|
4029
|
+
let littleReport = '\nRequest Post Data:\n';
|
|
4030
|
+
let success = true;
|
|
4031
|
+
|
|
4032
|
+
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
4033
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
4034
|
+
continue;
|
|
4035
|
+
}
|
|
4036
|
+
let keyFound = false;
|
|
4037
|
+
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
4038
|
+
|
|
4039
|
+
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
4040
|
+
if (key === expectedKey) {
|
|
4041
|
+
keyFound = true;
|
|
4042
|
+
if (expectedValue === undefined) {
|
|
4043
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
4044
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
4045
|
+
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
4046
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
4047
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
4048
|
+
} else {
|
|
4049
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
4050
|
+
success = false;
|
|
4051
|
+
}
|
|
4052
|
+
} else if (value === expectedValue) {
|
|
4053
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
4054
|
+
} else {
|
|
4055
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
4056
|
+
success = false;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
if (keyFound === false) {
|
|
4062
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
4063
|
+
success = false;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return success ? true : littleReport;
|
|
4068
|
+
};
|