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.
Files changed (281) hide show
  1. package/README.md +31 -30
  2. package/bin/codecept.js +1 -1
  3. package/lib/actor.js +6 -3
  4. package/lib/ai.js +180 -0
  5. package/lib/cli.js +13 -3
  6. package/lib/codecept.js +8 -0
  7. package/lib/colorUtils.js +10 -0
  8. package/lib/command/definitions.js +2 -7
  9. package/lib/command/dryRun.js +11 -2
  10. package/lib/command/generate.js +46 -3
  11. package/lib/command/info.js +24 -0
  12. package/lib/command/init.js +64 -6
  13. package/lib/command/interactive.js +15 -1
  14. package/lib/command/run-multiple/collection.js +17 -5
  15. package/lib/command/run-multiple.js +4 -2
  16. package/lib/command/run-workers.js +68 -5
  17. package/lib/command/run.js +7 -0
  18. package/lib/command/workers/runTests.js +39 -0
  19. package/lib/container.js +13 -3
  20. package/lib/data/context.js +14 -6
  21. package/lib/event.js +4 -0
  22. package/lib/helper/ApiDataFactory.js +2 -1
  23. package/lib/helper/Appium.js +116 -29
  24. package/lib/helper/Expect.js +422 -0
  25. package/lib/helper/FileSystem.js +1 -1
  26. package/lib/helper/GraphQL.js +25 -0
  27. package/lib/helper/JSONResponse.js +4 -4
  28. package/lib/helper/Nightmare.js +10 -5
  29. package/lib/helper/OpenAI.js +126 -0
  30. package/lib/helper/Playwright.js +1298 -229
  31. package/lib/helper/Protractor.js +12 -7
  32. package/lib/helper/Puppeteer.js +204 -64
  33. package/lib/helper/REST.js +15 -5
  34. package/lib/helper/TestCafe.js +45 -10
  35. package/lib/helper/WebDriver.js +252 -83
  36. package/lib/helper/errors/ElementNotFound.js +2 -1
  37. package/lib/helper/extras/PlaywrightReactVueLocator.js +38 -0
  38. package/lib/helper/scripts/blurElement.js +17 -0
  39. package/lib/helper/scripts/focusElement.js +17 -0
  40. package/lib/helper/scripts/highlightElement.js +20 -0
  41. package/lib/html.js +258 -0
  42. package/lib/interfaces/bdd.js +1 -1
  43. package/lib/interfaces/gherkin.js +37 -3
  44. package/lib/interfaces/scenarioConfig.js +1 -0
  45. package/lib/listener/retry.js +2 -1
  46. package/lib/locator.js +17 -4
  47. package/lib/mochaFactory.js +2 -1
  48. package/lib/output.js +1 -1
  49. package/lib/pause.js +78 -19
  50. package/lib/plugin/autoLogin.js +45 -10
  51. package/lib/plugin/debugErrors.js +67 -0
  52. package/lib/plugin/fakerTransform.js +4 -6
  53. package/lib/plugin/heal.js +209 -0
  54. package/lib/plugin/retryFailedStep.js +10 -1
  55. package/lib/plugin/retryTo.js +2 -4
  56. package/lib/plugin/screenshotOnFail.js +11 -2
  57. package/lib/plugin/selenoid.js +6 -1
  58. package/lib/plugin/standardActingHelpers.js +0 -2
  59. package/lib/plugin/stepByStepReport.js +2 -2
  60. package/lib/plugin/tryTo.js +5 -7
  61. package/lib/plugin/wdio.js +0 -1
  62. package/lib/recorder.js +22 -11
  63. package/lib/secret.js +5 -4
  64. package/lib/session.js +1 -1
  65. package/lib/step.js +36 -12
  66. package/lib/ui.js +5 -3
  67. package/lib/utils.js +22 -1
  68. package/lib/workers.js +83 -10
  69. package/package.json +117 -95
  70. package/translations/de-DE.js +5 -0
  71. package/translations/fr-FR.js +14 -1
  72. package/translations/it-IT.js +1 -0
  73. package/translations/ja-JP.js +14 -9
  74. package/translations/pl-PL.js +5 -0
  75. package/translations/pt-BR.js +1 -0
  76. package/translations/ru-RU.js +1 -0
  77. package/translations/zh-CN.js +5 -0
  78. package/translations/zh-TW.js +5 -0
  79. package/typings/index.d.ts +51 -15
  80. package/typings/promiseBasedTypes.d.ts +864 -802
  81. package/typings/types.d.ts +1339 -744
  82. package/CHANGELOG.md +0 -2427
  83. package/docs/advanced.md +0 -351
  84. package/docs/api.md +0 -323
  85. package/docs/basics.md +0 -980
  86. package/docs/bdd.md +0 -535
  87. package/docs/best.md +0 -237
  88. package/docs/books.md +0 -37
  89. package/docs/bootstrap.md +0 -135
  90. package/docs/build/ApiDataFactory.js +0 -409
  91. package/docs/build/Appium.js +0 -1938
  92. package/docs/build/FileSystem.js +0 -228
  93. package/docs/build/GraphQL.js +0 -204
  94. package/docs/build/GraphQLDataFactory.js +0 -309
  95. package/docs/build/JSONResponse.js +0 -338
  96. package/docs/build/Mochawesome.js +0 -71
  97. package/docs/build/Nightmare.js +0 -2145
  98. package/docs/build/Playwright.js +0 -3986
  99. package/docs/build/Polly.js +0 -42
  100. package/docs/build/Protractor.js +0 -2699
  101. package/docs/build/Puppeteer.js +0 -3710
  102. package/docs/build/REST.js +0 -334
  103. package/docs/build/SeleniumWebdriver.js +0 -76
  104. package/docs/build/TestCafe.js +0 -2057
  105. package/docs/build/WebDriver.js +0 -4017
  106. package/docs/changelog.md +0 -2436
  107. package/docs/commands.md +0 -254
  108. package/docs/community-helpers.md +0 -58
  109. package/docs/configuration.md +0 -157
  110. package/docs/continuous-integration.md +0 -22
  111. package/docs/custom-helpers.md +0 -306
  112. package/docs/data.md +0 -375
  113. package/docs/detox.md +0 -235
  114. package/docs/docker.md +0 -137
  115. package/docs/email.md +0 -183
  116. package/docs/examples.md +0 -149
  117. package/docs/helpers/ApiDataFactory.md +0 -266
  118. package/docs/helpers/Appium.md +0 -1312
  119. package/docs/helpers/Detox.md +0 -586
  120. package/docs/helpers/FileSystem.md +0 -152
  121. package/docs/helpers/GraphQL.md +0 -130
  122. package/docs/helpers/GraphQLDataFactory.md +0 -226
  123. package/docs/helpers/JSONResponse.md +0 -254
  124. package/docs/helpers/Mochawesome.md +0 -8
  125. package/docs/helpers/MockRequest.md +0 -377
  126. package/docs/helpers/Nightmare.md +0 -1256
  127. package/docs/helpers/Playwright.md +0 -2208
  128. package/docs/helpers/Polly.md +0 -44
  129. package/docs/helpers/Puppeteer-firefox.md +0 -86
  130. package/docs/helpers/Puppeteer.md +0 -2141
  131. package/docs/helpers/REST.md +0 -217
  132. package/docs/helpers/TestCafe.md +0 -1222
  133. package/docs/helpers/WebDriver.md +0 -2319
  134. package/docs/hooks.md +0 -340
  135. package/docs/index.md +0 -111
  136. package/docs/installation.md +0 -75
  137. package/docs/internal-api.md +0 -265
  138. package/docs/locators.md +0 -331
  139. package/docs/mobile-react-native-locators.md +0 -67
  140. package/docs/mobile.md +0 -297
  141. package/docs/nightmare.md +0 -223
  142. package/docs/pageobjects.md +0 -291
  143. package/docs/parallel.md +0 -232
  144. package/docs/playwright.md +0 -609
  145. package/docs/plugins.md +0 -1171
  146. package/docs/puppeteer.md +0 -316
  147. package/docs/quickstart.md +0 -163
  148. package/docs/react.md +0 -69
  149. package/docs/reports.md +0 -392
  150. package/docs/secrets.md +0 -30
  151. package/docs/shadow.md +0 -68
  152. package/docs/shared/keys.mustache +0 -31
  153. package/docs/shared/react.mustache +0 -1
  154. package/docs/testcafe.md +0 -174
  155. package/docs/translation.md +0 -247
  156. package/docs/tutorial.md +0 -271
  157. package/docs/typescript.md +0 -180
  158. package/docs/ui.md +0 -59
  159. package/docs/videos.md +0 -28
  160. package/docs/visual.md +0 -202
  161. package/docs/vue.md +0 -121
  162. package/docs/webapi/amOnPage.mustache +0 -11
  163. package/docs/webapi/appendField.mustache +0 -9
  164. package/docs/webapi/attachFile.mustache +0 -12
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -10
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -25
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeElement.mustache +0 -8
  177. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  178. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  179. package/docs/webapi/dontSeeInField.mustache +0 -11
  180. package/docs/webapi/dontSeeInSource.mustache +0 -8
  181. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  182. package/docs/webapi/doubleClick.mustache +0 -13
  183. package/docs/webapi/downloadFile.mustache +0 -12
  184. package/docs/webapi/dragAndDrop.mustache +0 -9
  185. package/docs/webapi/dragSlider.mustache +0 -11
  186. package/docs/webapi/executeAsyncScript.mustache +0 -24
  187. package/docs/webapi/executeScript.mustache +0 -26
  188. package/docs/webapi/fillField.mustache +0 -16
  189. package/docs/webapi/forceClick.mustache +0 -28
  190. package/docs/webapi/forceRightClick.mustache +0 -18
  191. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  192. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  193. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  194. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  195. package/docs/webapi/grabCookie.mustache +0 -11
  196. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  197. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  198. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  199. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  200. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  201. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  202. package/docs/webapi/grabGeoLocation.mustache +0 -8
  203. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  204. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  205. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  206. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  207. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  208. package/docs/webapi/grabPopupText.mustache +0 -5
  209. package/docs/webapi/grabSource.mustache +0 -8
  210. package/docs/webapi/grabTextFrom.mustache +0 -10
  211. package/docs/webapi/grabTextFromAll.mustache +0 -9
  212. package/docs/webapi/grabTitle.mustache +0 -8
  213. package/docs/webapi/grabValueFrom.mustache +0 -9
  214. package/docs/webapi/grabValueFromAll.mustache +0 -8
  215. package/docs/webapi/moveCursorTo.mustache +0 -12
  216. package/docs/webapi/openNewTab.mustache +0 -7
  217. package/docs/webapi/pressKey.mustache +0 -12
  218. package/docs/webapi/pressKeyDown.mustache +0 -12
  219. package/docs/webapi/pressKeyUp.mustache +0 -12
  220. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  221. package/docs/webapi/refreshPage.mustache +0 -6
  222. package/docs/webapi/resizeWindow.mustache +0 -6
  223. package/docs/webapi/rightClick.mustache +0 -14
  224. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  225. package/docs/webapi/saveScreenshot.mustache +0 -12
  226. package/docs/webapi/say.mustache +0 -10
  227. package/docs/webapi/scrollIntoView.mustache +0 -11
  228. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  229. package/docs/webapi/scrollPageToTop.mustache +0 -6
  230. package/docs/webapi/scrollTo.mustache +0 -12
  231. package/docs/webapi/see.mustache +0 -11
  232. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  233. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  234. package/docs/webapi/seeCookie.mustache +0 -8
  235. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  236. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  237. package/docs/webapi/seeElement.mustache +0 -8
  238. package/docs/webapi/seeElementInDOM.mustache +0 -8
  239. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  240. package/docs/webapi/seeInField.mustache +0 -12
  241. package/docs/webapi/seeInPopup.mustache +0 -8
  242. package/docs/webapi/seeInSource.mustache +0 -7
  243. package/docs/webapi/seeInTitle.mustache +0 -8
  244. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  245. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  246. package/docs/webapi/seeTextEquals.mustache +0 -9
  247. package/docs/webapi/seeTitleEquals.mustache +0 -8
  248. package/docs/webapi/selectOption.mustache +0 -21
  249. package/docs/webapi/setCookie.mustache +0 -16
  250. package/docs/webapi/setGeoLocation.mustache +0 -12
  251. package/docs/webapi/switchTo.mustache +0 -9
  252. package/docs/webapi/switchToNextTab.mustache +0 -10
  253. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  254. package/docs/webapi/type.mustache +0 -18
  255. package/docs/webapi/uncheckOption.mustache +0 -13
  256. package/docs/webapi/wait.mustache +0 -8
  257. package/docs/webapi/waitForClickable.mustache +0 -11
  258. package/docs/webapi/waitForDetached.mustache +0 -10
  259. package/docs/webapi/waitForElement.mustache +0 -11
  260. package/docs/webapi/waitForEnabled.mustache +0 -6
  261. package/docs/webapi/waitForFunction.mustache +0 -17
  262. package/docs/webapi/waitForInvisible.mustache +0 -10
  263. package/docs/webapi/waitForText.mustache +0 -13
  264. package/docs/webapi/waitForValue.mustache +0 -10
  265. package/docs/webapi/waitForVisible.mustache +0 -10
  266. package/docs/webapi/waitInUrl.mustache +0 -9
  267. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  268. package/docs/webapi/waitToHide.mustache +0 -10
  269. package/docs/webapi/waitUrlEquals.mustache +0 -10
  270. package/docs/webdriver.md +0 -657
  271. package/docs/wiki/Books-&-Posts.md +0 -27
  272. package/docs/wiki/Community-Helpers-&-Plugins.md +0 -49
  273. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +0 -29
  274. package/docs/wiki/Examples.md +0 -139
  275. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +0 -68
  276. package/docs/wiki/Home.md +0 -16
  277. package/docs/wiki/Release-Process.md +0 -24
  278. package/docs/wiki/Roadmap.md +0 -23
  279. package/docs/wiki/Tests.md +0 -1393
  280. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +0 -153
  281. package/docs/wiki/Videos.md +0 -19
@@ -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/React');
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 {string} [browser] - a browser to test on, either: `chromium`, `firefox`, `webkit`, `electron`. Default: chromium.
58
- * @prop {boolean} [show=false] - show browser window.
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 {string} [waitForNavigation] - When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle`. Choose one of those options is possible. See [Playwright API](https://playwright.dev/docs/api/class-page#page-wait-for-navigation).
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 {string} [colorScheme] - default color scheme. Possible values: `dark` | `light` | `no-preference`.
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 a complete information on test execution and includes DOM snapshots, screenshots, and network requests logged during run.
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: 0,
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: 5,
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
- const existingPages = await this.browserContext.pages();
455
- mainPage = existingPages[0] || await this.browserContext.newPage();
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
- const contexts = await this.browser.contexts();
487
- const currentContext = contexts[0];
488
- if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) {
489
- this.storageState = await currentContext.storageState();
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
- await Promise.all(contexts.map(c => c.close()));
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
- browserContext = await this.browser.newContext(Object.assign(this.options, config));
523
- page = await browserContext.newPage();
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
- this.browserContext = context;
537
- const existingPages = await context.pages();
538
- this.sessionPages[this.activeSessionName] = existingPages[0];
539
- return this._setPage(this.sessionPages[this.activeSessionName]);
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 argumen
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(locator);
787
- this.withinLocator = new Locator(locator);
885
+ await this.switchTo(frame);
886
+ this.withinLocator = new Locator(frame);
788
887
  return;
789
888
  }
790
889
 
791
- const els = await this._locate(locator);
792
- assertElementExists(els, locator);
793
- this.context = els[0];
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.haveRequestHeaders({
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 haveRequestHeaders(customHeaders) {
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 els = await this._locate(locator);
897
- assertElementExists(els, locator);
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(els[0]);
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
- * {{> dragAndDrop }}
1007
+ * {{> focus }}
907
1008
  *
908
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-drag-and-drop) can be passed as 3rd argument.
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
- * > By default option `force: true` is set
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 = { force: true }) {
918
- const src = new Locator(srcElement, 'css');
919
- const dst = new Locator(destElement, 'css');
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
- return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options);
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, body.offsetHeight,
969
- html.clientHeight, html.scrollHeight, html.offsetHeight,
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 els = await this._locate(locator);
986
- assertElementExists(els, locator, 'Element');
987
- await els[0].scrollIntoViewIfNeeded();
988
- const elementCoordinates = await clickablePoint(els[0]);
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
- * Find a checkbox by providing human readable text:
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 readable text:
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 readable text:
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} [opts] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1519
+ * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-page#page-click) for click available as 3rd argument.
1286
1520
  *
1287
- * Examples:
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, opts = {}) {
1299
- return proceedClick.call(this, locator, context, opts);
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
- const tag = await el.getProperty('tagName').then(el => el.jsonValue());
1459
- const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
1460
- if (tag === 'INPUT' || tag === 'TEXTAREA') {
1461
- await this._evaluateHandeInContext(el => el.value = '', el);
1462
- } else if (editable) {
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
- * {{> clearField }}
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(field) {
1473
- return this.fillField(field, '');
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
- return proceedSeeInField.call(this, 'assert', field, value);
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
- return proceedSeeInField.call(this, 'negate', field, value);
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
- for (const key in option) {
1532
- const opt = xpathLocator.literal(option[key]);
1533
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
1534
- if (optEl.length) {
1535
- this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
1536
- continue;
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#class-browsercontext
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
- * but you can pass in array or object.
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
- let context = this.page;
1756
- if (this.context && this.context.constructor.name === 'Frame') {
1757
- context = this.context; // switching to iframe context
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 context.evaluate.apply(context, [fn, arg]);
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.getProperty('innerText')).jsonValue());
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.getProperty('value').then(t => t.jsonValue())));
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.$eval('xpath=.', element => element.innerHTML, 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.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
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
- const commands = [];
1876
- res.forEach((el) => {
1877
- Object.keys(cssPropertiesCamelCase).forEach((prop) => {
1878
- commands.push(el.$eval('xpath=.', (el) => {
1879
- const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
1880
- return JSON.parse(JSON.stringify(style));
1881
- }, el)
1882
- .then((props) => {
1883
- if (isColorProperty(prop)) {
1884
- return convertColorToRGBA(props[prop]);
1885
- }
1886
- return props[prop];
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
- if (val[i] !== values[i]) return false;
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
- .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
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 (val[i] !== values[i]) return false;
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._locate(locator);
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[0]);
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
- const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
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._locate(locator);
2236
+ const res = await this._locateElement(locator);
1990
2237
  assertElementExists(res, locator);
1991
- if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
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
- const outputFile = screenshotOutputFolder(fileName);
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 activeSessionPage = this.sessionPages[this.activeSessionName];
2259
+ for (const sessionName in this.sessionPages) {
2260
+ const activeSessionPage = this.sessionPages[sessionName];
2261
+ outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`);
2008
2262
 
2009
- if (activeSessionPage) {
2010
- return activeSessionPage.screenshot({
2011
- path: outputFile,
2012
- fullPage: fullPageOption,
2013
- type: 'png',
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(), `${test.title}_${sessionName}.failed`);
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(), `${test.title}_${sessionName}.passed`);
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
- const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'attached' });
2229
- return waiter.catch((err) => {
2230
- throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
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
- const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'visible' });
2244
- return waiter.catch((err) => {
2245
- throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
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
- const waiter = context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'hidden' });
2257
- return waiter.catch((err) => {
2258
- throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
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
- return context.waitForSelector(buildLocatorString(locator), { timeout: waitTimeout, state: 'hidden' }).catch((err) => {
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 === 'Frame') {
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
- waiter = contextObject.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`, { timeout: waitTimeout, state: 'visible' });
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
- waiter = contextObject.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: waitTimeout });
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 = childFrames[locator];
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
- const els = await this._locate(locator);
2417
- assertElementExists(els, locator);
2418
- const contentFrame = await els[0].contentFrame();
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 = els[0];
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 {*} opts
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 waitForNavigation(opts = {}) {
2454
- opts = {
2829
+ async waitForURL(url, options = {}) {
2830
+ options = {
2455
2831
  timeout: this.options.getPageTimeout,
2456
2832
  waitUntil: this.options.waitForNavigation,
2457
- ...opts,
2833
+ ...options,
2458
2834
  };
2459
- return this.page.waitForNavigation(opts);
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
- waiter = context.waitForSelector(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`, { timeout: waitTimeout, state: 'detached' });
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 els = await this._locate(locator);
2508
- assertElementExists(els, locator);
2509
- const rect = await els[0].boundingBox();
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
- module.exports = Playwright;
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
- function buildLocatorString(locator) {
2549
- if (locator.isCustom()) {
2550
- return `${locator.type}=${locator.value}`;
2551
- } if (locator.isXPath()) {
2552
- // dont rely on heuristics of playwright for figuring out xpath
2553
- return `xpath=${locator.value}`;
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
- return matcher.$$(buildLocatorString(locator));
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
- let el = await this.context;
3557
+ const el = await this.context;
2637
3558
 
2638
- if (el && !el.getProperty) {
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.getProperty('innerText').then(p => p.jsonValue())));
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.getProperty('tagName').then(el => el.jsonValue());
2719
- const fieldType = await el.getProperty('type').then(el => el.jsonValue());
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.getProperty('value').then(el => el.jsonValue()));
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.getProperty('multiple')) {
2741
- const selectedOptions = await el.$$('option:checked');
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
- const fieldVal = await el.inputValue();
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.getProperty('value').then(el => el.jsonValue());
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.getProperty('type').then(el => !!el && el.jsonValue());
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.getProperty('selected').then(el => el.jsonValue());
3727
+ return element.getAttribute('selected');
2802
3728
  }
2803
3729
 
2804
3730
  function isFrameLocator(locator) {
2805
3731
  locator = new Locator(locator);
2806
- if (locator.isFrame()) return locator.value;
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}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`;
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}${Date.now()}_${clearString(name)}`.slice(0, 245)}.zip`;
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
+ };