codeceptjs 3.4.1 → 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 +31 -30
- package/bin/codecept.js +1 -1
- package/lib/actor.js +6 -3
- package/lib/ai.js +180 -0
- package/lib/cli.js +13 -3
- package/lib/codecept.js +8 -0
- package/lib/colorUtils.js +10 -0
- package/lib/command/definitions.js +2 -7
- package/lib/command/dryRun.js +11 -2
- package/lib/command/generate.js +46 -3
- package/lib/command/info.js +24 -0
- package/lib/command/init.js +64 -6
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-multiple/collection.js +17 -5
- package/lib/command/run-multiple.js +4 -2
- package/lib/command/run-workers.js +68 -5
- package/lib/command/run.js +7 -0
- package/lib/command/workers/runTests.js +39 -0
- package/lib/container.js +13 -3
- 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 +116 -29
- package/lib/helper/Expect.js +422 -0
- package/lib/helper/FileSystem.js +1 -1
- package/lib/helper/GraphQL.js +25 -0
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Nightmare.js +10 -5
- package/lib/helper/OpenAI.js +126 -0
- package/lib/helper/Playwright.js +1298 -229
- package/lib/helper/Protractor.js +12 -7
- package/lib/helper/Puppeteer.js +204 -64
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +45 -10
- package/lib/helper/WebDriver.js +252 -83
- 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 +20 -0
- package/lib/html.js +258 -0
- package/lib/interfaces/bdd.js +1 -1
- package/lib/interfaces/gherkin.js +37 -3
- package/lib/interfaces/scenarioConfig.js +1 -0
- package/lib/listener/retry.js +2 -1
- package/lib/locator.js +17 -4
- package/lib/mochaFactory.js +2 -1
- package/lib/output.js +1 -1
- package/lib/pause.js +78 -19
- package/lib/plugin/autoLogin.js +45 -10
- package/lib/plugin/debugErrors.js +67 -0
- package/lib/plugin/fakerTransform.js +4 -6
- package/lib/plugin/heal.js +209 -0
- package/lib/plugin/retryFailedStep.js +10 -1
- package/lib/plugin/retryTo.js +2 -4
- package/lib/plugin/screenshotOnFail.js +11 -2
- 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 +22 -11
- package/lib/secret.js +5 -4
- package/lib/session.js +1 -1
- package/lib/step.js +36 -12
- package/lib/ui.js +5 -3
- package/lib/utils.js +22 -1
- package/lib/workers.js +83 -10
- package/package.json +117 -95
- 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 +14 -9
- 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 +51 -15
- package/typings/promiseBasedTypes.d.ts +864 -802
- package/typings/types.d.ts +1339 -744
- package/CHANGELOG.md +0 -2427
- package/docs/advanced.md +0 -351
- 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 -1938
- 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 -2145
- package/docs/build/Playwright.js +0 -3986
- package/docs/build/Polly.js +0 -42
- package/docs/build/Protractor.js +0 -2699
- package/docs/build/Puppeteer.js +0 -3710
- package/docs/build/REST.js +0 -334
- package/docs/build/SeleniumWebdriver.js +0 -76
- package/docs/build/TestCafe.js +0 -2057
- package/docs/build/WebDriver.js +0 -4017
- package/docs/changelog.md +0 -2436
- 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 -1312
- 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 -1256
- package/docs/helpers/Playwright.md +0 -2208
- package/docs/helpers/Polly.md +0 -44
- package/docs/helpers/Puppeteer-firefox.md +0 -86
- package/docs/helpers/Puppeteer.md +0 -2141
- package/docs/helpers/REST.md +0 -217
- package/docs/helpers/TestCafe.md +0 -1222
- package/docs/helpers/WebDriver.md +0 -2319
- 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 -297
- package/docs/nightmare.md +0 -223
- package/docs/pageobjects.md +0 -291
- package/docs/parallel.md +0 -232
- package/docs/playwright.md +0 -609
- package/docs/plugins.md +0 -1171
- 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 -30
- 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 -9
- 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 -18
- 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
|
@@ -2,7 +2,10 @@ const path = require('path');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
|
|
4
4
|
const Helper = require('@codeceptjs/helper');
|
|
5
|
+
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
const assert = require('assert');
|
|
5
7
|
const Locator = require('../locator');
|
|
8
|
+
const store = require('../store');
|
|
6
9
|
const recorder = require('../recorder');
|
|
7
10
|
const stringIncludes = require('../assert/include').includes;
|
|
8
11
|
const { urlEquals } = require('../assert/equal');
|
|
@@ -20,6 +23,7 @@ const {
|
|
|
20
23
|
isModifierKey,
|
|
21
24
|
clearString,
|
|
22
25
|
requireWithFallback,
|
|
26
|
+
normalizeSpacesInString,
|
|
23
27
|
} = require('../utils');
|
|
24
28
|
const {
|
|
25
29
|
isColorProperty,
|
|
@@ -29,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
29
33
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
30
34
|
const Popup = require('./extras/Popup');
|
|
31
35
|
const Console = require('./extras/Console');
|
|
32
|
-
const findReact = require('./extras/
|
|
36
|
+
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
|
|
33
37
|
|
|
34
38
|
let playwright;
|
|
35
39
|
let perfTiming;
|
|
@@ -49,13 +53,13 @@ const pathSeparator = path.sep;
|
|
|
49
53
|
/**
|
|
50
54
|
* ## Configuration
|
|
51
55
|
*
|
|
52
|
-
* This helper should be configured in codecept.conf.js
|
|
56
|
+
* This helper should be configured in codecept.conf.(js|ts)
|
|
53
57
|
*
|
|
54
58
|
* @typedef PlaywrightConfig
|
|
55
59
|
* @type {object}
|
|
56
|
-
* @prop {string} url - base url of website to be tested
|
|
57
|
-
* @prop {
|
|
58
|
-
* @prop {boolean} [show=
|
|
60
|
+
* @prop {string} [url] - base url of website to be tested
|
|
61
|
+
* @prop {'chromium' | 'firefox'| 'webkit' | 'electron'} [browser='chromium'] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
|
|
62
|
+
* @prop {boolean} [show=true] - show browser window.
|
|
59
63
|
* @prop {string|boolean} [restart=false] - restart strategy between tests. Possible values:
|
|
60
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.
|
|
61
65
|
* * 'browser' or **true** - closes browser and opens it again between tests.
|
|
@@ -72,13 +76,13 @@ const pathSeparator = path.sep;
|
|
|
72
76
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'.
|
|
73
77
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to 'session'.
|
|
74
78
|
* @prop {number} [waitForAction] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
75
|
-
* @prop {
|
|
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).
|
|
76
80
|
* @prop {number} [pressKeyDelay=10] - Delay between key presses in ms. Used when calling Playwrights page.type(...) in fillField/appendField
|
|
77
81
|
* @prop {number} [getPageTimeout] - config option to set maximum navigation time in milliseconds.
|
|
78
82
|
* @prop {number} [waitForTimeout] - default wait* timeout in ms. Default: 1000.
|
|
79
83
|
* @prop {object} [basicAuth] - the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
|
|
80
84
|
* @prop {string} [windowSize] - default window size. Set a dimension like `640x480`.
|
|
81
|
-
* @prop {
|
|
85
|
+
* @prop {'dark' | 'light' | 'no-preference'} [colorScheme] - default color scheme. Possible values: `dark` | `light` | `no-preference`.
|
|
82
86
|
* @prop {string} [userAgent] - user-agent string.
|
|
83
87
|
* @prop {string} [locale] - locale string. Example: 'en-GB', 'de-DE', 'fr-FR', ...
|
|
84
88
|
* @prop {boolean} [manualStart] - do not start browser before a test, start it manually inside a helper with `this.helpers["Playwright"]._startBrowser()`.
|
|
@@ -88,6 +92,9 @@ const pathSeparator = path.sep;
|
|
|
88
92
|
* @prop {any} [channel] - (While Playwright can operate against the stock Google Chrome and Microsoft Edge browsers available on the machine. In particular, current Playwright version will support Stable and Beta channels of these browsers. See [Google Chrome & Microsoft Edge](https://playwright.dev/docs/browsers/#google-chrome--microsoft-edge).
|
|
89
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).
|
|
90
94
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
95
|
+
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
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).
|
|
91
98
|
*/
|
|
92
99
|
const config = {};
|
|
93
100
|
|
|
@@ -110,6 +117,10 @@ const config = {};
|
|
|
110
117
|
* npm i playwright-core@^1.18 --save
|
|
111
118
|
* ```
|
|
112
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
|
+
*
|
|
113
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.
|
|
114
125
|
*
|
|
115
126
|
*
|
|
@@ -125,12 +136,27 @@ const config = {};
|
|
|
125
136
|
*
|
|
126
137
|
* #### Trace Recording Customization
|
|
127
138
|
*
|
|
128
|
-
* Trace recording provides
|
|
139
|
+
* Trace recording provides complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
|
|
129
140
|
* Traces will be saved to `output/trace`
|
|
130
141
|
*
|
|
131
142
|
* * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
|
|
132
143
|
* * `keepTraceForPassedTests`: - save trace for passed tests
|
|
133
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
|
+
*
|
|
134
160
|
* #### Example #1: Wait for 0 network connections.
|
|
135
161
|
*
|
|
136
162
|
* ```js
|
|
@@ -201,6 +227,7 @@ const config = {};
|
|
|
201
227
|
* url: "http://localhost",
|
|
202
228
|
* show: true // headless mode not supported for extensions
|
|
203
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
|
|
204
231
|
* userDataDir: '/tmp/playwright-tmp', // necessary to launch the browser in normal mode instead of incognito,
|
|
205
232
|
* args: [
|
|
206
233
|
* `--disable-extensions-except=${pathToExtension}`,
|
|
@@ -255,6 +282,22 @@ const config = {};
|
|
|
255
282
|
* }
|
|
256
283
|
* ```
|
|
257
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
|
+
*
|
|
258
301
|
* Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
|
|
259
302
|
*
|
|
260
303
|
* ## Access From Helpers
|
|
@@ -290,6 +333,17 @@ class Playwright extends Helper {
|
|
|
290
333
|
this.electronSessions = [];
|
|
291
334
|
this.storageState = null;
|
|
292
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
|
+
|
|
293
347
|
// override defaults with config
|
|
294
348
|
this._setConfig(config);
|
|
295
349
|
}
|
|
@@ -308,7 +362,7 @@ class Playwright extends Helper {
|
|
|
308
362
|
ignoreLog: ['warning', 'log'],
|
|
309
363
|
uniqueScreenshotNames: false,
|
|
310
364
|
manualStart: false,
|
|
311
|
-
getPageTimeout:
|
|
365
|
+
getPageTimeout: 30000,
|
|
312
366
|
waitForNavigation: 'load',
|
|
313
367
|
restart: false,
|
|
314
368
|
keepCookies: false,
|
|
@@ -316,7 +370,8 @@ class Playwright extends Helper {
|
|
|
316
370
|
show: false,
|
|
317
371
|
defaultPopupAction: 'accept',
|
|
318
372
|
use: { actionTimeout: 0 },
|
|
319
|
-
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,
|
|
320
375
|
};
|
|
321
376
|
|
|
322
377
|
config = Object.assign(defaults, config);
|
|
@@ -361,22 +416,31 @@ class Playwright extends Helper {
|
|
|
361
416
|
}
|
|
362
417
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint;
|
|
363
418
|
this.isElectron = this.options.browser === 'electron';
|
|
364
|
-
this.userDataDir = this.playwrightOptions.userDataDir;
|
|
419
|
+
this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined;
|
|
365
420
|
this.isCDPConnection = this.playwrightOptions.cdpConnection;
|
|
366
421
|
popupStore.defaultAction = this.options.defaultPopupAction;
|
|
367
422
|
}
|
|
368
423
|
|
|
369
424
|
static _config() {
|
|
370
425
|
return [
|
|
371
|
-
{ name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
|
|
372
|
-
{
|
|
373
|
-
name: 'show', message: 'Show browser window', default: true, type: 'confirm',
|
|
374
|
-
},
|
|
375
426
|
{
|
|
376
427
|
name: 'browser',
|
|
377
428
|
message: 'Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron',
|
|
378
429
|
default: 'chromium',
|
|
379
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
|
+
},
|
|
380
444
|
];
|
|
381
445
|
}
|
|
382
446
|
|
|
@@ -407,9 +471,10 @@ class Playwright extends Helper {
|
|
|
407
471
|
}
|
|
408
472
|
}
|
|
409
473
|
|
|
410
|
-
async _before() {
|
|
474
|
+
async _before(test) {
|
|
475
|
+
this.currentRunningTest = test;
|
|
411
476
|
recorder.retry({
|
|
412
|
-
retries:
|
|
477
|
+
retries: process.env.FAILED_STEP_RETRIES || 3,
|
|
413
478
|
when: err => {
|
|
414
479
|
if (!err || typeof (err.message) !== 'string') {
|
|
415
480
|
return false;
|
|
@@ -425,7 +490,7 @@ class Playwright extends Helper {
|
|
|
425
490
|
this.isAuthenticated = false;
|
|
426
491
|
if (this.isElectron) {
|
|
427
492
|
this.browserContext = this.browser.context();
|
|
428
|
-
} else if (this.userDataDir) {
|
|
493
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
429
494
|
this.browserContext = this.browser;
|
|
430
495
|
} else {
|
|
431
496
|
const contextOptions = {
|
|
@@ -437,13 +502,24 @@ class Playwright extends Helper {
|
|
|
437
502
|
contextOptions.httpCredentials = this.options.basicAuth;
|
|
438
503
|
this.isAuthenticated = true;
|
|
439
504
|
}
|
|
505
|
+
if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
|
|
440
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
|
+
}
|
|
441
516
|
if (this.storageState) contextOptions.storageState = this.storageState;
|
|
442
517
|
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
|
|
443
518
|
if (this.options.locale) contextOptions.locale = this.options.locale;
|
|
444
519
|
if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme;
|
|
520
|
+
this.contextOptions = contextOptions;
|
|
445
521
|
if (!this.browserContext || !restartsSession()) {
|
|
446
|
-
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
|
|
447
523
|
}
|
|
448
524
|
}
|
|
449
525
|
|
|
@@ -451,8 +527,17 @@ class Playwright extends Helper {
|
|
|
451
527
|
if (this.isElectron) {
|
|
452
528
|
mainPage = await this.browser.firstWindow();
|
|
453
529
|
} else {
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
}
|
|
456
541
|
}
|
|
457
542
|
await targetCreatedHandler.call(this, mainPage);
|
|
458
543
|
|
|
@@ -483,13 +568,15 @@ class Playwright extends Helper {
|
|
|
483
568
|
|
|
484
569
|
// close other sessions
|
|
485
570
|
try {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this.
|
|
490
|
-
|
|
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
|
+
}
|
|
491
577
|
|
|
492
|
-
|
|
578
|
+
await Promise.all(contexts.map(c => c.close()));
|
|
579
|
+
}
|
|
493
580
|
} catch (e) {
|
|
494
581
|
console.log(e);
|
|
495
582
|
}
|
|
@@ -519,8 +606,16 @@ class Playwright extends Helper {
|
|
|
519
606
|
browserContext = browser.context();
|
|
520
607
|
page = await browser.firstWindow();
|
|
521
608
|
} else {
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
}
|
|
524
619
|
}
|
|
525
620
|
|
|
526
621
|
if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true });
|
|
@@ -533,10 +628,12 @@ class Playwright extends Helper {
|
|
|
533
628
|
// is closed by _after
|
|
534
629
|
},
|
|
535
630
|
loadVars: async (context) => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
+
}
|
|
540
637
|
},
|
|
541
638
|
restoreVars: async (session) => {
|
|
542
639
|
this.withinLocator = null;
|
|
@@ -570,7 +667,7 @@ class Playwright extends Helper {
|
|
|
570
667
|
* ```
|
|
571
668
|
*
|
|
572
669
|
* @param {string} description used to show in logs.
|
|
573
|
-
* @param {function} fn async function that executed with Playwright helper as
|
|
670
|
+
* @param {function} fn async function that executed with Playwright helper as arguments
|
|
574
671
|
*/
|
|
575
672
|
usePlaywrightTo(description, fn) {
|
|
576
673
|
return this._useTo(...arguments);
|
|
@@ -727,7 +824,7 @@ class Playwright extends Helper {
|
|
|
727
824
|
}
|
|
728
825
|
throw err;
|
|
729
826
|
}
|
|
730
|
-
} else if (this.userDataDir) {
|
|
827
|
+
} else if (this.playwrightOptions.userDataDir) {
|
|
731
828
|
this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions);
|
|
732
829
|
} else {
|
|
733
830
|
this.browser = await playwright[this.options.browser].launch(this.playwrightOptions);
|
|
@@ -760,9 +857,11 @@ class Playwright extends Helper {
|
|
|
760
857
|
|
|
761
858
|
async _stopBrowser() {
|
|
762
859
|
this.withinLocator = null;
|
|
763
|
-
this._setPage(null);
|
|
860
|
+
await this._setPage(null);
|
|
764
861
|
this.context = null;
|
|
862
|
+
this.frame = null;
|
|
765
863
|
popupStore.clear();
|
|
864
|
+
if (this.options.recordHar) await this.browserContext.close();
|
|
766
865
|
await this.browser.close();
|
|
767
866
|
}
|
|
768
867
|
|
|
@@ -783,14 +882,14 @@ class Playwright extends Helper {
|
|
|
783
882
|
await this.switchTo(null);
|
|
784
883
|
return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve());
|
|
785
884
|
}
|
|
786
|
-
await this.switchTo(
|
|
787
|
-
this.withinLocator = new Locator(
|
|
885
|
+
await this.switchTo(frame);
|
|
886
|
+
this.withinLocator = new Locator(frame);
|
|
788
887
|
return;
|
|
789
888
|
}
|
|
790
889
|
|
|
791
|
-
const
|
|
792
|
-
assertElementExists(
|
|
793
|
-
this.context =
|
|
890
|
+
const el = await this._locateElement(locator);
|
|
891
|
+
assertElementExists(el, locator);
|
|
892
|
+
this.context = el;
|
|
794
893
|
this.contextLocator = locator;
|
|
795
894
|
|
|
796
895
|
this.withinLocator = new Locator(locator);
|
|
@@ -800,6 +899,7 @@ class Playwright extends Helper {
|
|
|
800
899
|
this.withinLocator = null;
|
|
801
900
|
this.context = await this.page;
|
|
802
901
|
this.contextLocator = null;
|
|
902
|
+
this.frame = null;
|
|
803
903
|
}
|
|
804
904
|
|
|
805
905
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
@@ -847,10 +947,9 @@ class Playwright extends Helper {
|
|
|
847
947
|
}
|
|
848
948
|
|
|
849
949
|
/**
|
|
850
|
-
* {{> resizeWindow }}
|
|
851
950
|
*
|
|
852
951
|
* Unlike other drivers Playwright changes the size of a viewport, not the window!
|
|
853
|
-
* 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.
|
|
854
953
|
* It also can't maximize a window.
|
|
855
954
|
*
|
|
856
955
|
* Update configuration to change real window size on start:
|
|
@@ -860,6 +959,8 @@ class Playwright extends Helper {
|
|
|
860
959
|
* // @codeceptjs/configure package must be installed
|
|
861
960
|
* { setWindowSize } = require('@codeceptjs/configure');
|
|
862
961
|
* ````
|
|
962
|
+
*
|
|
963
|
+
* {{> resizeWindow }}
|
|
863
964
|
*/
|
|
864
965
|
async resizeWindow(width, height) {
|
|
865
966
|
if (width === 'maximize') {
|
|
@@ -874,14 +975,14 @@ class Playwright extends Helper {
|
|
|
874
975
|
* Set headers for all next requests
|
|
875
976
|
*
|
|
876
977
|
* ```js
|
|
877
|
-
* I.
|
|
978
|
+
* I.setPlaywrightRequestHeaders({
|
|
878
979
|
* 'X-Sent-By': 'CodeceptJS',
|
|
879
980
|
* });
|
|
880
981
|
* ```
|
|
881
982
|
*
|
|
882
983
|
* @param {object} customHeaders headers to set
|
|
883
984
|
*/
|
|
884
|
-
async
|
|
985
|
+
async setPlaywrightRequestHeaders(customHeaders) {
|
|
885
986
|
if (!customHeaders) {
|
|
886
987
|
throw new Error('Cannot send empty headers.');
|
|
887
988
|
}
|
|
@@ -893,32 +994,103 @@ class Playwright extends Helper {
|
|
|
893
994
|
*
|
|
894
995
|
*/
|
|
895
996
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
896
|
-
const
|
|
897
|
-
assertElementExists(
|
|
997
|
+
const el = await this._locateElement(locator);
|
|
998
|
+
assertElementExists(el, locator);
|
|
898
999
|
|
|
899
1000
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
900
|
-
const { x, y } = await clickablePoint(
|
|
1001
|
+
const { x, y } = await clickablePoint(el);
|
|
901
1002
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
902
1003
|
return this._waitForAction();
|
|
903
1004
|
}
|
|
904
1005
|
|
|
905
1006
|
/**
|
|
906
|
-
* {{>
|
|
1007
|
+
* {{> focus }}
|
|
907
1008
|
*
|
|
908
|
-
|
|
1009
|
+
*/
|
|
1010
|
+
async focus(locator, options = {}) {
|
|
1011
|
+
const el = await this._locateElement(locator);
|
|
1012
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
1013
|
+
|
|
1014
|
+
await el.focus(options);
|
|
1015
|
+
return this._waitForAction();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* {{> blur }}
|
|
1020
|
+
*
|
|
1021
|
+
*/
|
|
1022
|
+
async blur(locator, options = {}) {
|
|
1023
|
+
const el = await this._locateElement(locator);
|
|
1024
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
1025
|
+
|
|
1026
|
+
await el.blur(options);
|
|
1027
|
+
return this._waitForAction();
|
|
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
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
909
1063
|
*
|
|
910
1064
|
* ```js
|
|
911
1065
|
* // specify coordinates for source position
|
|
912
1066
|
* I.dragAndDrop('img.src', 'img.dst', { sourcePosition: {x: 10, y: 10} })
|
|
913
1067
|
* ```
|
|
914
1068
|
*
|
|
915
|
-
* >
|
|
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
|
+
*
|
|
916
1074
|
*/
|
|
917
|
-
async dragAndDrop(srcElement, destElement, options
|
|
918
|
-
const src = new Locator(srcElement
|
|
919
|
-
const dst = new Locator(destElement
|
|
1075
|
+
async dragAndDrop(srcElement, destElement, options) {
|
|
1076
|
+
const src = new Locator(srcElement);
|
|
1077
|
+
const dst = new Locator(destElement);
|
|
1078
|
+
|
|
1079
|
+
if (options) {
|
|
1080
|
+
return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const _smallWaitInMs = 600;
|
|
1084
|
+
await this.page.locator(buildLocatorString(src)).hover();
|
|
1085
|
+
await this.page.mouse.down();
|
|
1086
|
+
await this.page.waitForTimeout(_smallWaitInMs);
|
|
1087
|
+
|
|
1088
|
+
const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox();
|
|
920
1089
|
|
|
921
|
-
|
|
1090
|
+
await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2);
|
|
1091
|
+
await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } });
|
|
1092
|
+
await this.page.waitForTimeout(_smallWaitInMs);
|
|
1093
|
+
await this.page.mouse.up();
|
|
922
1094
|
}
|
|
923
1095
|
|
|
924
1096
|
/**
|
|
@@ -948,6 +1120,33 @@ class Playwright extends Helper {
|
|
|
948
1120
|
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
|
|
949
1121
|
}
|
|
950
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
|
+
|
|
951
1150
|
/**
|
|
952
1151
|
* {{> scrollPageToTop }}
|
|
953
1152
|
*/
|
|
@@ -965,8 +1164,11 @@ class Playwright extends Helper {
|
|
|
965
1164
|
const body = document.body;
|
|
966
1165
|
const html = document.documentElement;
|
|
967
1166
|
window.scrollTo(0, Math.max(
|
|
968
|
-
body.scrollHeight,
|
|
969
|
-
|
|
1167
|
+
body.scrollHeight,
|
|
1168
|
+
body.offsetHeight,
|
|
1169
|
+
html.clientHeight,
|
|
1170
|
+
html.scrollHeight,
|
|
1171
|
+
html.offsetHeight,
|
|
970
1172
|
));
|
|
971
1173
|
});
|
|
972
1174
|
}
|
|
@@ -982,10 +1184,10 @@ class Playwright extends Helper {
|
|
|
982
1184
|
}
|
|
983
1185
|
|
|
984
1186
|
if (locator) {
|
|
985
|
-
const
|
|
986
|
-
assertElementExists(
|
|
987
|
-
await
|
|
988
|
-
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);
|
|
989
1191
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
990
1192
|
} else {
|
|
991
1193
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1049,11 +1251,27 @@ class Playwright extends Helper {
|
|
|
1049
1251
|
*/
|
|
1050
1252
|
async _locate(locator) {
|
|
1051
1253
|
const context = await this.context || await this._getContext();
|
|
1254
|
+
|
|
1255
|
+
if (this.frame) return findElements(this.frame, locator);
|
|
1256
|
+
|
|
1052
1257
|
return findElements(context, locator);
|
|
1053
1258
|
}
|
|
1054
1259
|
|
|
1055
1260
|
/**
|
|
1056
|
-
*
|
|
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:
|
|
1057
1275
|
* NOTE: Assumes the checkable element exists
|
|
1058
1276
|
*
|
|
1059
1277
|
* ```js
|
|
@@ -1068,7 +1286,7 @@ class Playwright extends Helper {
|
|
|
1068
1286
|
}
|
|
1069
1287
|
|
|
1070
1288
|
/**
|
|
1071
|
-
* Find a clickable element by providing human
|
|
1289
|
+
* Find a clickable element by providing human-readable text:
|
|
1072
1290
|
*
|
|
1073
1291
|
* ```js
|
|
1074
1292
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1080,7 +1298,7 @@ class Playwright extends Helper {
|
|
|
1080
1298
|
}
|
|
1081
1299
|
|
|
1082
1300
|
/**
|
|
1083
|
-
* Find field elements by providing human
|
|
1301
|
+
* Find field elements by providing human-readable text:
|
|
1084
1302
|
*
|
|
1085
1303
|
* ```js
|
|
1086
1304
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1090,6 +1308,22 @@ class Playwright extends Helper {
|
|
|
1090
1308
|
return findFields.call(this, locator);
|
|
1091
1309
|
}
|
|
1092
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
|
+
|
|
1093
1327
|
/**
|
|
1094
1328
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1095
1329
|
*
|
|
@@ -1282,9 +1516,9 @@ class Playwright extends Helper {
|
|
|
1282
1516
|
/**
|
|
1283
1517
|
* {{> click }}
|
|
1284
1518
|
*
|
|
1285
|
-
* @param {any} [
|
|
1519
|
+
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
|
|
1286
1520
|
*
|
|
1287
|
-
*
|
|
1521
|
+
* @example
|
|
1288
1522
|
*
|
|
1289
1523
|
* ```js
|
|
1290
1524
|
* // click on element at position
|
|
@@ -1295,8 +1529,8 @@ class Playwright extends Helper {
|
|
|
1295
1529
|
* ```
|
|
1296
1530
|
*
|
|
1297
1531
|
*/
|
|
1298
|
-
async click(locator, context = null,
|
|
1299
|
-
return proceedClick.call(this, locator, context,
|
|
1532
|
+
async click(locator, context = null, options = {}) {
|
|
1533
|
+
return proceedClick.call(this, locator, context, options);
|
|
1300
1534
|
}
|
|
1301
1535
|
|
|
1302
1536
|
/**
|
|
@@ -1317,8 +1551,6 @@ class Playwright extends Helper {
|
|
|
1317
1551
|
|
|
1318
1552
|
/**
|
|
1319
1553
|
* {{> doubleClick }}
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
1554
|
*/
|
|
1323
1555
|
async doubleClick(locator, context = null) {
|
|
1324
1556
|
return proceedClick.call(this, locator, context, { clickCount: 2 });
|
|
@@ -1326,15 +1558,12 @@ class Playwright extends Helper {
|
|
|
1326
1558
|
|
|
1327
1559
|
/**
|
|
1328
1560
|
* {{> rightClick }}
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
1561
|
*/
|
|
1332
1562
|
async rightClick(locator, context = null) {
|
|
1333
1563
|
return proceedClick.call(this, locator, context, { button: 'right' });
|
|
1334
1564
|
}
|
|
1335
1565
|
|
|
1336
1566
|
/**
|
|
1337
|
-
* {{> checkOption }}
|
|
1338
1567
|
*
|
|
1339
1568
|
* [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-check) for check available as 3rd argument.
|
|
1340
1569
|
*
|
|
@@ -1345,6 +1574,9 @@ class Playwright extends Helper {
|
|
|
1345
1574
|
* I.checkOption('Agree', '.signup', { position: { x: 5, y: 5 } })
|
|
1346
1575
|
* ```
|
|
1347
1576
|
* > ⚠️ To avoid flakiness, option `force: true` is set by default
|
|
1577
|
+
*
|
|
1578
|
+
* {{> checkOption }}
|
|
1579
|
+
*
|
|
1348
1580
|
*/
|
|
1349
1581
|
async checkOption(field, context = null, options = { force: true }) {
|
|
1350
1582
|
const elm = await this._locateCheckable(field, context);
|
|
@@ -1353,7 +1585,6 @@ class Playwright extends Helper {
|
|
|
1353
1585
|
}
|
|
1354
1586
|
|
|
1355
1587
|
/**
|
|
1356
|
-
* {{> uncheckOption }}
|
|
1357
1588
|
*
|
|
1358
1589
|
* [Additional options](https://playwright.dev/docs/api/class-elementhandle#element-handle-uncheck) for uncheck available as 3rd argument.
|
|
1359
1590
|
*
|
|
@@ -1364,6 +1595,8 @@ class Playwright extends Helper {
|
|
|
1364
1595
|
* I.uncheckOption('Agree', '.signup', { position: { x: 5, y: 5 } })
|
|
1365
1596
|
* ```
|
|
1366
1597
|
* > ⚠️ To avoid flakiness, option `force: true` is set by default
|
|
1598
|
+
*
|
|
1599
|
+
* {{> uncheckOption }}
|
|
1367
1600
|
*/
|
|
1368
1601
|
async uncheckOption(field, context = null, options = { force: true }) {
|
|
1369
1602
|
const elm = await this._locateCheckable(field, context);
|
|
@@ -1404,9 +1637,10 @@ class Playwright extends Helper {
|
|
|
1404
1637
|
}
|
|
1405
1638
|
|
|
1406
1639
|
/**
|
|
1407
|
-
* {{> pressKeyWithKeyNormalization }}
|
|
1408
1640
|
*
|
|
1409
1641
|
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([GoogleChrome/Puppeteer#1313](https://github.com/GoogleChrome/puppeteer/issues/1313)).
|
|
1642
|
+
*
|
|
1643
|
+
* {{> pressKeyWithKeyNormalization }}
|
|
1410
1644
|
*/
|
|
1411
1645
|
async pressKey(key) {
|
|
1412
1646
|
const modifiers = [];
|
|
@@ -1438,6 +1672,7 @@ class Playwright extends Helper {
|
|
|
1438
1672
|
*/
|
|
1439
1673
|
async type(keys, delay = null) {
|
|
1440
1674
|
if (!Array.isArray(keys)) {
|
|
1675
|
+
keys = keys.toString();
|
|
1441
1676
|
keys = keys.split('');
|
|
1442
1677
|
}
|
|
1443
1678
|
|
|
@@ -1455,34 +1690,55 @@ class Playwright extends Helper {
|
|
|
1455
1690
|
const els = await findFields.call(this, field);
|
|
1456
1691
|
assertElementExists(els, field, 'Field');
|
|
1457
1692
|
const el = els[0];
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1464
|
-
}
|
|
1693
|
+
|
|
1694
|
+
await el.clear();
|
|
1695
|
+
|
|
1696
|
+
await highlightActiveElement.call(this, el);
|
|
1697
|
+
|
|
1465
1698
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1699
|
+
|
|
1466
1700
|
return this._waitForAction();
|
|
1467
1701
|
}
|
|
1468
1702
|
|
|
1469
1703
|
/**
|
|
1470
|
-
*
|
|
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
|
+
*
|
|
1717
|
+
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
1718
|
+
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1471
1719
|
*/
|
|
1472
|
-
async clearField(
|
|
1473
|
-
|
|
1720
|
+
async clearField(locator, options = {}) {
|
|
1721
|
+
const els = await findFields.call(this, locator);
|
|
1722
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
1723
|
+
|
|
1724
|
+
const el = els[0];
|
|
1725
|
+
|
|
1726
|
+
await highlightActiveElement.call(this, el);
|
|
1727
|
+
|
|
1728
|
+
await el.clear();
|
|
1729
|
+
|
|
1730
|
+
return this._waitForAction();
|
|
1474
1731
|
}
|
|
1475
1732
|
|
|
1476
1733
|
/**
|
|
1477
1734
|
* {{> appendField }}
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
1735
|
*/
|
|
1481
1736
|
async appendField(field, value) {
|
|
1482
1737
|
const els = await findFields.call(this, field);
|
|
1483
1738
|
assertElementExists(els, field, 'Field');
|
|
1739
|
+
await highlightActiveElement.call(this, els[0]);
|
|
1484
1740
|
await els[0].press('End');
|
|
1485
|
-
await els[0].type(value, { delay: this.options.pressKeyDelay });
|
|
1741
|
+
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay });
|
|
1486
1742
|
return this._waitForAction();
|
|
1487
1743
|
}
|
|
1488
1744
|
|
|
@@ -1490,14 +1746,16 @@ class Playwright extends Helper {
|
|
|
1490
1746
|
* {{> seeInField }}
|
|
1491
1747
|
*/
|
|
1492
1748
|
async seeInField(field, value) {
|
|
1493
|
-
|
|
1749
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1750
|
+
return proceedSeeInField.call(this, 'assert', field, _value);
|
|
1494
1751
|
}
|
|
1495
1752
|
|
|
1496
1753
|
/**
|
|
1497
1754
|
* {{> dontSeeInField }}
|
|
1498
1755
|
*/
|
|
1499
1756
|
async dontSeeInField(field, value) {
|
|
1500
|
-
|
|
1757
|
+
const _value = (typeof value === 'boolean') ? value : value.toString();
|
|
1758
|
+
return proceedSeeInField.call(this, 'negate', field, _value);
|
|
1501
1759
|
}
|
|
1502
1760
|
|
|
1503
1761
|
/**
|
|
@@ -1523,28 +1781,19 @@ class Playwright extends Helper {
|
|
|
1523
1781
|
const els = await findFields.call(this, select);
|
|
1524
1782
|
assertElementExists(els, select, 'Selectable field');
|
|
1525
1783
|
const el = els[0];
|
|
1526
|
-
if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
|
|
1527
|
-
throw new Error('Element is not <select>');
|
|
1528
|
-
}
|
|
1529
|
-
if (!Array.isArray(option)) option = [option];
|
|
1530
1784
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
|
|
1539
|
-
if (optEl.length) {
|
|
1540
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1541
|
-
}
|
|
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;
|
|
1542
1792
|
}
|
|
1543
|
-
await this._evaluateHandeInContext((element) => {
|
|
1544
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1545
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1546
|
-
}, el);
|
|
1547
1793
|
|
|
1794
|
+
if (!Array.isArray(option)) option = [optionToSelect];
|
|
1795
|
+
|
|
1796
|
+
await el.selectOption(option);
|
|
1548
1797
|
return this._waitForAction();
|
|
1549
1798
|
}
|
|
1550
1799
|
|
|
@@ -1706,9 +1955,9 @@ class Playwright extends Helper {
|
|
|
1706
1955
|
}
|
|
1707
1956
|
|
|
1708
1957
|
/**
|
|
1709
|
-
* {{> grabCookie }}
|
|
1710
|
-
*
|
|
1711
1958
|
* Returns cookie in JSON format. If name not passed returns all cookies for this domain.
|
|
1959
|
+
*
|
|
1960
|
+
* {{> grabCookie }}
|
|
1712
1961
|
*/
|
|
1713
1962
|
async grabCookie(name) {
|
|
1714
1963
|
const cookies = await this.browserContext.cookies();
|
|
@@ -1722,7 +1971,7 @@ class Playwright extends Helper {
|
|
|
1722
1971
|
*/
|
|
1723
1972
|
async clearCookie() {
|
|
1724
1973
|
// Playwright currently doesn't support to delete a certain cookie
|
|
1725
|
-
// 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
|
|
1726
1975
|
if (!this.browserContext) return;
|
|
1727
1976
|
return this.browserContext.clearCookies();
|
|
1728
1977
|
}
|
|
@@ -1739,8 +1988,8 @@ class Playwright extends Helper {
|
|
|
1739
1988
|
* ```js
|
|
1740
1989
|
* I.executeScript(({x, y}) => x + y, {x, y});
|
|
1741
1990
|
* ```
|
|
1742
|
-
* You can pass only one parameter into a function
|
|
1743
|
-
*
|
|
1991
|
+
* You can pass only one parameter into a function,
|
|
1992
|
+
* or you can pass in array or object.
|
|
1744
1993
|
*
|
|
1745
1994
|
* ```js
|
|
1746
1995
|
* I.executeScript(([x, y]) => x + y, [x, y]);
|
|
@@ -1752,11 +2001,11 @@ class Playwright extends Helper {
|
|
|
1752
2001
|
* @returns {Promise<any>}
|
|
1753
2002
|
*/
|
|
1754
2003
|
async executeScript(fn, arg) {
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2004
|
+
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2005
|
+
// switching to iframe context
|
|
2006
|
+
return this.context.locator(':root').evaluate(fn, arg);
|
|
1758
2007
|
}
|
|
1759
|
-
return
|
|
2008
|
+
return this.page.evaluate.apply(this.page, [fn, arg]);
|
|
1760
2009
|
}
|
|
1761
2010
|
|
|
1762
2011
|
/**
|
|
@@ -1795,7 +2044,7 @@ class Playwright extends Helper {
|
|
|
1795
2044
|
const els = await this._locate(locator);
|
|
1796
2045
|
const texts = [];
|
|
1797
2046
|
for (const el of els) {
|
|
1798
|
-
texts.push(await (await el.
|
|
2047
|
+
texts.push(await (await el.innerText()));
|
|
1799
2048
|
}
|
|
1800
2049
|
this.debug(`Matched ${els.length} elements`);
|
|
1801
2050
|
return texts;
|
|
@@ -1817,7 +2066,7 @@ class Playwright extends Helper {
|
|
|
1817
2066
|
async grabValueFromAll(locator) {
|
|
1818
2067
|
const els = await findFields.call(this, locator);
|
|
1819
2068
|
this.debug(`Matched ${els.length} elements`);
|
|
1820
|
-
return Promise.all(els.map(el => el.
|
|
2069
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
1821
2070
|
}
|
|
1822
2071
|
|
|
1823
2072
|
/**
|
|
@@ -1836,7 +2085,7 @@ class Playwright extends Helper {
|
|
|
1836
2085
|
async grabHTMLFromAll(locator) {
|
|
1837
2086
|
const els = await this._locate(locator);
|
|
1838
2087
|
this.debug(`Matched ${els.length} elements`);
|
|
1839
|
-
return Promise.all(els.map(el => el
|
|
2088
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
1840
2089
|
}
|
|
1841
2090
|
|
|
1842
2091
|
/**
|
|
@@ -1857,7 +2106,7 @@ class Playwright extends Helper {
|
|
|
1857
2106
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1858
2107
|
const els = await this._locate(locator);
|
|
1859
2108
|
this.debug(`Matched ${els.length} elements`);
|
|
1860
|
-
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)));
|
|
1861
2110
|
|
|
1862
2111
|
return cssValues;
|
|
1863
2112
|
}
|
|
@@ -1872,28 +2121,26 @@ class Playwright extends Helper {
|
|
|
1872
2121
|
|
|
1873
2122
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
1874
2123
|
const elemAmount = res.length;
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
});
|
|
1889
|
-
});
|
|
1890
|
-
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
|
+
|
|
1891
2137
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
1892
2138
|
if (!Array.isArray(props)) props = [props];
|
|
1893
2139
|
let chunked = chunkArray(props, values.length);
|
|
1894
2140
|
chunked = chunked.filter((val) => {
|
|
1895
2141
|
for (let i = 0; i < val.length; ++i) {
|
|
1896
|
-
|
|
2142
|
+
// eslint-disable-next-line eqeqeq
|
|
2143
|
+
if (val[i] != values[i]) return false;
|
|
1897
2144
|
}
|
|
1898
2145
|
return true;
|
|
1899
2146
|
});
|
|
@@ -1913,7 +2160,7 @@ class Playwright extends Helper {
|
|
|
1913
2160
|
res.forEach((el) => {
|
|
1914
2161
|
Object.keys(attributes).forEach((prop) => {
|
|
1915
2162
|
commands.push(el
|
|
1916
|
-
|
|
2163
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
1917
2164
|
});
|
|
1918
2165
|
});
|
|
1919
2166
|
let attrs = await Promise.all(commands);
|
|
@@ -1922,7 +2169,8 @@ class Playwright extends Helper {
|
|
|
1922
2169
|
let chunked = chunkArray(attrs, values.length);
|
|
1923
2170
|
chunked = chunked.filter((val) => {
|
|
1924
2171
|
for (let i = 0; i < val.length; ++i) {
|
|
1925
|
-
if
|
|
2172
|
+
// if the attribute doesn't exist, returns false as well
|
|
2173
|
+
if (!val[i] || !val[i].includes(values[i])) return false;
|
|
1926
2174
|
}
|
|
1927
2175
|
return true;
|
|
1928
2176
|
});
|
|
@@ -1934,11 +2182,11 @@ class Playwright extends Helper {
|
|
|
1934
2182
|
*
|
|
1935
2183
|
*/
|
|
1936
2184
|
async dragSlider(locator, offsetX = 0) {
|
|
1937
|
-
const src = await this.
|
|
2185
|
+
const src = await this._locateElement(locator);
|
|
1938
2186
|
assertElementExists(src, locator, 'Slider Element');
|
|
1939
2187
|
|
|
1940
2188
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
1941
|
-
const sliderSource = await clickablePoint(src
|
|
2189
|
+
const sliderSource = await clickablePoint(src);
|
|
1942
2190
|
|
|
1943
2191
|
// Drag start point
|
|
1944
2192
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -1972,8 +2220,7 @@ class Playwright extends Helper {
|
|
|
1972
2220
|
const array = [];
|
|
1973
2221
|
|
|
1974
2222
|
for (let index = 0; index < els.length; index++) {
|
|
1975
|
-
|
|
1976
|
-
array.push(await a.jsonValue());
|
|
2223
|
+
array.push(await els[index].getAttribute(attr));
|
|
1977
2224
|
}
|
|
1978
2225
|
|
|
1979
2226
|
return array;
|
|
@@ -1986,10 +2233,9 @@ class Playwright extends Helper {
|
|
|
1986
2233
|
async saveElementScreenshot(locator, fileName) {
|
|
1987
2234
|
const outputFile = screenshotOutputFolder(fileName);
|
|
1988
2235
|
|
|
1989
|
-
const res = await this.
|
|
2236
|
+
const res = await this._locateElement(locator);
|
|
1990
2237
|
assertElementExists(res, locator);
|
|
1991
|
-
|
|
1992
|
-
const elem = res[0];
|
|
2238
|
+
const elem = res;
|
|
1993
2239
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
1994
2240
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
1995
2241
|
}
|
|
@@ -1999,23 +2245,32 @@ class Playwright extends Helper {
|
|
|
1999
2245
|
*/
|
|
2000
2246
|
async saveScreenshot(fileName, fullPage) {
|
|
2001
2247
|
const fullPageOption = fullPage || this.options.fullPageScreenshots;
|
|
2002
|
-
|
|
2248
|
+
let outputFile = screenshotOutputFolder(fileName);
|
|
2003
2249
|
|
|
2004
2250
|
this.debug(`Screenshot is saving to ${outputFile}`);
|
|
2005
2251
|
|
|
2252
|
+
await this.page.screenshot({
|
|
2253
|
+
path: outputFile,
|
|
2254
|
+
fullPage: fullPageOption,
|
|
2255
|
+
type: 'png',
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2006
2258
|
if (this.activeSessionName) {
|
|
2007
|
-
const
|
|
2259
|
+
for (const sessionName in this.sessionPages) {
|
|
2260
|
+
const activeSessionPage = this.sessionPages[sessionName];
|
|
2261
|
+
outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
|
|
2008
2262
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2263
|
+
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`);
|
|
2264
|
+
|
|
2265
|
+
if (activeSessionPage) {
|
|
2266
|
+
await activeSessionPage.screenshot({
|
|
2267
|
+
path: outputFile,
|
|
2268
|
+
fullPage: fullPageOption,
|
|
2269
|
+
type: 'png',
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2015
2272
|
}
|
|
2016
2273
|
}
|
|
2017
|
-
|
|
2018
|
-
return this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png' });
|
|
2019
2274
|
}
|
|
2020
2275
|
|
|
2021
2276
|
/**
|
|
@@ -2083,9 +2338,13 @@ class Playwright extends Helper {
|
|
|
2083
2338
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`);
|
|
2084
2339
|
for (const sessionName in this.sessionPages) {
|
|
2085
2340
|
if (!this.sessionPages[sessionName].context) continue;
|
|
2086
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context
|
|
2341
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
2087
2342
|
}
|
|
2088
2343
|
}
|
|
2344
|
+
|
|
2345
|
+
if (this.options.recordHar) {
|
|
2346
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2347
|
+
}
|
|
2089
2348
|
}
|
|
2090
2349
|
|
|
2091
2350
|
async _passed(test) {
|
|
@@ -2106,13 +2365,17 @@ class Playwright extends Helper {
|
|
|
2106
2365
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`);
|
|
2107
2366
|
for (const sessionName in this.sessionPages) {
|
|
2108
2367
|
if (!this.sessionPages[sessionName].context) continue;
|
|
2109
|
-
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context
|
|
2368
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`);
|
|
2110
2369
|
}
|
|
2111
2370
|
}
|
|
2112
2371
|
} else {
|
|
2113
2372
|
await this.browserContext.tracing.stop();
|
|
2114
2373
|
}
|
|
2115
2374
|
}
|
|
2375
|
+
|
|
2376
|
+
if (this.options.recordHar) {
|
|
2377
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2378
|
+
}
|
|
2116
2379
|
}
|
|
2117
2380
|
|
|
2118
2381
|
/**
|
|
@@ -2225,25 +2488,42 @@ class Playwright extends Helper {
|
|
|
2225
2488
|
locator = new Locator(locator, 'css');
|
|
2226
2489
|
|
|
2227
2490
|
const context = await this._getContext();
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
+
}
|
|
2232
2496
|
}
|
|
2233
2497
|
|
|
2234
2498
|
/**
|
|
2235
|
-
* {{> waitForVisible }}
|
|
2236
|
-
*
|
|
2237
2499
|
* This method accepts [React selectors](https://codecept.io/react).
|
|
2500
|
+
*
|
|
2501
|
+
* {{> waitForVisible }}
|
|
2238
2502
|
*/
|
|
2239
2503
|
async waitForVisible(locator, sec) {
|
|
2240
2504
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2241
2505
|
locator = new Locator(locator, 'css');
|
|
2242
2506
|
const context = await this._getContext();
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
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
|
+
}
|
|
2247
2527
|
}
|
|
2248
2528
|
|
|
2249
2529
|
/**
|
|
@@ -2253,10 +2533,27 @@ class Playwright extends Helper {
|
|
|
2253
2533
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2254
2534
|
locator = new Locator(locator, 'css');
|
|
2255
2535
|
const context = await this._getContext();
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
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
|
+
}
|
|
2260
2557
|
}
|
|
2261
2558
|
|
|
2262
2559
|
/**
|
|
@@ -2266,13 +2563,47 @@ class Playwright extends Helper {
|
|
|
2266
2563
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2267
2564
|
locator = new Locator(locator, 'css');
|
|
2268
2565
|
const context = await this._getContext();
|
|
2269
|
-
|
|
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) => {
|
|
2270
2583
|
throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2271
2584
|
});
|
|
2272
2585
|
}
|
|
2273
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
|
+
|
|
2274
2605
|
async _getContext() {
|
|
2275
|
-
if (this.context && this.context.constructor.name === '
|
|
2606
|
+
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2276
2607
|
return this.context;
|
|
2277
2608
|
}
|
|
2278
2609
|
return this.page;
|
|
@@ -2333,7 +2664,12 @@ class Playwright extends Helper {
|
|
|
2333
2664
|
if (context) {
|
|
2334
2665
|
const locator = new Locator(context, 'css');
|
|
2335
2666
|
if (!locator.isXPath()) {
|
|
2336
|
-
|
|
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
|
+
}
|
|
2337
2673
|
}
|
|
2338
2674
|
|
|
2339
2675
|
if (locator.isXPath()) {
|
|
@@ -2345,11 +2681,19 @@ class Playwright extends Helper {
|
|
|
2345
2681
|
}, [locator.value, text, $XPath.toString()], { timeout: waitTimeout });
|
|
2346
2682
|
}
|
|
2347
2683
|
} else {
|
|
2348
|
-
|
|
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`);
|
|
2349
2696
|
}
|
|
2350
|
-
return waiter.catch((err) => {
|
|
2351
|
-
throw new Error(`Text "${text}" was not found on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2352
|
-
});
|
|
2353
2697
|
}
|
|
2354
2698
|
|
|
2355
2699
|
/**
|
|
@@ -2399,29 +2743,42 @@ class Playwright extends Helper {
|
|
|
2399
2743
|
}
|
|
2400
2744
|
|
|
2401
2745
|
if (locator >= 0 && locator < childFrames.length) {
|
|
2402
|
-
this.context =
|
|
2746
|
+
this.context = await this.page.frameLocator('iframe').nth(locator);
|
|
2403
2747
|
this.contextLocator = locator;
|
|
2404
2748
|
} else {
|
|
2405
2749
|
throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath');
|
|
2406
2750
|
}
|
|
2407
2751
|
return;
|
|
2408
2752
|
}
|
|
2753
|
+
|
|
2409
2754
|
if (!locator) {
|
|
2410
2755
|
this.context = this.page;
|
|
2411
2756
|
this.contextLocator = null;
|
|
2757
|
+
this.frame = null;
|
|
2412
2758
|
return;
|
|
2413
2759
|
}
|
|
2414
2760
|
|
|
2415
2761
|
// iframe by selector
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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;
|
|
2419
2776
|
|
|
2420
2777
|
if (contentFrame) {
|
|
2421
2778
|
this.context = contentFrame;
|
|
2422
2779
|
this.contextLocator = null;
|
|
2423
2780
|
} else {
|
|
2424
|
-
this.context =
|
|
2781
|
+
this.context = this.page.frame(this.page.frames()[1].name());
|
|
2425
2782
|
this.contextLocator = locator;
|
|
2426
2783
|
}
|
|
2427
2784
|
}
|
|
@@ -2444,19 +2801,38 @@ class Playwright extends Helper {
|
|
|
2444
2801
|
}
|
|
2445
2802
|
|
|
2446
2803
|
/**
|
|
2447
|
-
* Waits for navigation to finish. By default takes configured `waitForNavigation` option.
|
|
2804
|
+
* Waits for navigation to finish. By default, it takes configured `waitForNavigation` option.
|
|
2448
2805
|
*
|
|
2449
2806
|
* See [Playwright's reference](https://playwright.dev/docs/api/class-page?_highlight=waitfornavi#pagewaitfornavigationoptions)
|
|
2450
2807
|
*
|
|
2451
|
-
* @param {*}
|
|
2808
|
+
* @param {*} options
|
|
2809
|
+
*/
|
|
2810
|
+
async waitForNavigation(options = {}) {
|
|
2811
|
+
console.log(`waitForNavigation deprecated:
|
|
2812
|
+
* This method is inherently racy, please use 'waitForURL' instead.`);
|
|
2813
|
+
options = {
|
|
2814
|
+
timeout: this.options.getPageTimeout,
|
|
2815
|
+
waitUntil: this.options.waitForNavigation,
|
|
2816
|
+
...options,
|
|
2817
|
+
};
|
|
2818
|
+
return this.page.waitForNavigation(options);
|
|
2819
|
+
}
|
|
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
|
|
2452
2828
|
*/
|
|
2453
|
-
async
|
|
2454
|
-
|
|
2829
|
+
async waitForURL(url, options = {}) {
|
|
2830
|
+
options = {
|
|
2455
2831
|
timeout: this.options.getPageTimeout,
|
|
2456
2832
|
waitUntil: this.options.waitForNavigation,
|
|
2457
|
-
...
|
|
2833
|
+
...options,
|
|
2458
2834
|
};
|
|
2459
|
-
return this.page.
|
|
2835
|
+
return this.page.waitForURL(url, options);
|
|
2460
2836
|
}
|
|
2461
2837
|
|
|
2462
2838
|
async waitUntilExists(locator, sec) {
|
|
@@ -2476,17 +2852,21 @@ class Playwright extends Helper {
|
|
|
2476
2852
|
let waiter;
|
|
2477
2853
|
const context = await this._getContext();
|
|
2478
2854
|
if (!locator.isXPath()) {
|
|
2479
|
-
|
|
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
|
+
}
|
|
2480
2860
|
} else {
|
|
2481
2861
|
const visibleFn = function ([locator, $XPath]) {
|
|
2482
2862
|
eval($XPath); // eslint-disable-line no-eval
|
|
2483
2863
|
return $XPath(null, locator).length === 0;
|
|
2484
2864
|
};
|
|
2485
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
|
+
});
|
|
2486
2869
|
}
|
|
2487
|
-
return waiter.catch((err) => {
|
|
2488
|
-
throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`);
|
|
2489
|
-
});
|
|
2490
2870
|
}
|
|
2491
2871
|
|
|
2492
2872
|
async _waitForAction() {
|
|
@@ -2504,9 +2884,9 @@ class Playwright extends Helper {
|
|
|
2504
2884
|
* {{> grabElementBoundingRect }}
|
|
2505
2885
|
*/
|
|
2506
2886
|
async grabElementBoundingRect(locator, prop) {
|
|
2507
|
-
const
|
|
2508
|
-
assertElementExists(
|
|
2509
|
-
const rect = await
|
|
2887
|
+
const el = await this._locateElement(locator);
|
|
2888
|
+
assertElementExists(el, locator);
|
|
2889
|
+
const rect = await el.boundingBox();
|
|
2510
2890
|
if (prop) return rect[prop];
|
|
2511
2891
|
return rect;
|
|
2512
2892
|
}
|
|
@@ -2541,24 +2921,559 @@ class Playwright extends Helper {
|
|
|
2541
2921
|
async stopMockingRoute(url, handler) {
|
|
2542
2922
|
return this.browserContext.unroute(...arguments);
|
|
2543
2923
|
}
|
|
2544
|
-
}
|
|
2545
2924
|
|
|
2546
|
-
|
|
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;
|
|
2547
2939
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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);
|
|
2953
|
+
}
|
|
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
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
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
|
+
}
|
|
3273
|
+
} else {
|
|
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.');
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
return this.webSocketMessages;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
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}`;
|
|
2554
3460
|
}
|
|
2555
3461
|
return locator.simplify();
|
|
2556
3462
|
}
|
|
2557
3463
|
|
|
2558
3464
|
async function findElements(matcher, locator) {
|
|
2559
3465
|
if (locator.react) return findReact(matcher, locator);
|
|
3466
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
2560
3467
|
locator = new Locator(locator, 'css');
|
|
2561
|
-
|
|
3468
|
+
|
|
3469
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
async function findElement(matcher, locator) {
|
|
3473
|
+
if (locator.react) return findReact(matcher, locator);
|
|
3474
|
+
locator = new Locator(locator, 'css');
|
|
3475
|
+
|
|
3476
|
+
return matcher.locator(buildLocatorString(locator)).first();
|
|
2562
3477
|
}
|
|
2563
3478
|
|
|
2564
3479
|
async function getVisibleElements(elements) {
|
|
@@ -2587,6 +3502,9 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2587
3502
|
} else {
|
|
2588
3503
|
assertElementExists(els, locator, 'Clickable element');
|
|
2589
3504
|
}
|
|
3505
|
+
|
|
3506
|
+
await highlightActiveElement.call(this, els[0]);
|
|
3507
|
+
|
|
2590
3508
|
/*
|
|
2591
3509
|
using the force true options itself but instead dispatching a click
|
|
2592
3510
|
*/
|
|
@@ -2598,14 +3516,16 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2598
3516
|
}
|
|
2599
3517
|
const promises = [];
|
|
2600
3518
|
if (options.waitForNavigation) {
|
|
2601
|
-
promises.push(this.waitForNavigation
|
|
3519
|
+
promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation }));
|
|
2602
3520
|
}
|
|
2603
3521
|
promises.push(this._waitForAction());
|
|
3522
|
+
|
|
2604
3523
|
return Promise.all(promises);
|
|
2605
3524
|
}
|
|
2606
3525
|
|
|
2607
3526
|
async function findClickable(matcher, locator) {
|
|
2608
3527
|
if (locator.react) return findReact(matcher, locator);
|
|
3528
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
2609
3529
|
|
|
2610
3530
|
locator = new Locator(locator);
|
|
2611
3531
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|
|
@@ -2632,28 +3552,25 @@ async function findClickable(matcher, locator) {
|
|
|
2632
3552
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
2633
3553
|
let description;
|
|
2634
3554
|
let allText;
|
|
3555
|
+
|
|
2635
3556
|
if (!context) {
|
|
2636
|
-
|
|
3557
|
+
const el = await this.context;
|
|
2637
3558
|
|
|
2638
|
-
|
|
2639
|
-
// Fallback to body
|
|
2640
|
-
el = await this.context.$('body');
|
|
2641
|
-
}
|
|
3559
|
+
allText = el.constructor.name !== 'Locator' ? [await el.locator('body').innerText()] : [await el.innerText()];
|
|
2642
3560
|
|
|
2643
|
-
allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
|
|
2644
3561
|
description = 'web application';
|
|
2645
3562
|
} else {
|
|
2646
3563
|
const locator = new Locator(context, 'css');
|
|
2647
3564
|
description = `element ${locator.toString()}`;
|
|
2648
3565
|
const els = await this._locate(locator);
|
|
2649
3566
|
assertElementExists(els, locator.toString());
|
|
2650
|
-
allText = await Promise.all(els.map(el => el.
|
|
3567
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
2651
3568
|
}
|
|
2652
3569
|
|
|
2653
3570
|
if (strict) {
|
|
2654
3571
|
return allText.map(elText => equals(description)[assertType](text, elText));
|
|
2655
3572
|
}
|
|
2656
|
-
return stringIncludes(description)[assertType](text, allText.join(' | '));
|
|
3573
|
+
return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
|
|
2657
3574
|
}
|
|
2658
3575
|
|
|
2659
3576
|
async function findCheckable(locator, context) {
|
|
@@ -2715,15 +3632,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2715
3632
|
const els = await findFields.call(this, field);
|
|
2716
3633
|
assertElementExists(els, field, 'Field');
|
|
2717
3634
|
const el = els[0];
|
|
2718
|
-
const tag = await el.
|
|
2719
|
-
const fieldType = await el.
|
|
3635
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
3636
|
+
const fieldType = await el.getAttribute('type');
|
|
2720
3637
|
|
|
2721
3638
|
const proceedMultiple = async (elements) => {
|
|
2722
3639
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
2723
3640
|
|
|
2724
3641
|
const elementValues = [];
|
|
2725
3642
|
for (const element of fields) {
|
|
2726
|
-
elementValues.push(await element.
|
|
3643
|
+
elementValues.push(await element.inputValue());
|
|
2727
3644
|
}
|
|
2728
3645
|
|
|
2729
3646
|
if (typeof value === 'boolean') {
|
|
@@ -2737,8 +3654,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2737
3654
|
};
|
|
2738
3655
|
|
|
2739
3656
|
if (tag === 'SELECT') {
|
|
2740
|
-
if (await el.
|
|
2741
|
-
const selectedOptions = await el
|
|
3657
|
+
if (await el.getAttribute('multiple')) {
|
|
3658
|
+
const selectedOptions = await el.all('option:checked');
|
|
2742
3659
|
if (!selectedOptions.length) return null;
|
|
2743
3660
|
|
|
2744
3661
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -2762,14 +3679,23 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2762
3679
|
return proceedMultiple(els[0]);
|
|
2763
3680
|
}
|
|
2764
3681
|
|
|
2765
|
-
|
|
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
|
+
|
|
2766
3692
|
return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
|
|
2767
3693
|
}
|
|
2768
3694
|
|
|
2769
3695
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
2770
3696
|
const matches = [];
|
|
2771
3697
|
for (const element of elements) {
|
|
2772
|
-
const val = await element.
|
|
3698
|
+
const val = await element.getAttribute('value');
|
|
2773
3699
|
let isSelected = true;
|
|
2774
3700
|
if (onlySelected) {
|
|
2775
3701
|
isSelected = await elementSelected(element);
|
|
@@ -2793,17 +3719,19 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
2793
3719
|
}
|
|
2794
3720
|
|
|
2795
3721
|
async function elementSelected(element) {
|
|
2796
|
-
const type = await element.
|
|
3722
|
+
const type = await element.getAttribute('type');
|
|
2797
3723
|
|
|
2798
3724
|
if (type === 'checkbox' || type === 'radio') {
|
|
2799
3725
|
return element.isChecked();
|
|
2800
3726
|
}
|
|
2801
|
-
return element.
|
|
3727
|
+
return element.getAttribute('selected');
|
|
2802
3728
|
}
|
|
2803
3729
|
|
|
2804
3730
|
function isFrameLocator(locator) {
|
|
2805
3731
|
locator = new Locator(locator);
|
|
2806
|
-
if (locator.isFrame())
|
|
3732
|
+
if (locator.isFrame()) {
|
|
3733
|
+
return locator.value;
|
|
3734
|
+
}
|
|
2807
3735
|
return false;
|
|
2808
3736
|
}
|
|
2809
3737
|
|
|
@@ -2982,7 +3910,7 @@ async function refreshContextSession() {
|
|
|
2982
3910
|
|
|
2983
3911
|
async function saveVideoForPage(page, name) {
|
|
2984
3912
|
if (!page.video()) return null;
|
|
2985
|
-
const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${
|
|
3913
|
+
const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`;
|
|
2986
3914
|
page.video().saveAs(fileName).then(() => {
|
|
2987
3915
|
if (!page) return;
|
|
2988
3916
|
page.video().delete().catch(e => {});
|
|
@@ -2993,7 +3921,148 @@ async function saveVideoForPage(page, name) {
|
|
|
2993
3921
|
async function saveTraceForContext(context, name) {
|
|
2994
3922
|
if (!context) return;
|
|
2995
3923
|
if (!context.tracing) return;
|
|
2996
|
-
const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${
|
|
3924
|
+
const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`;
|
|
2997
3925
|
await context.tracing.stop({ path: fileName });
|
|
2998
3926
|
return fileName;
|
|
2999
3927
|
}
|
|
3928
|
+
|
|
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
|
+
}
|
|
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
|
+
};
|