codeceptjs 3.5.0 → 3.5.1-2.beta.7

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