codeceptjs 3.5.12-beta.1 → 3.5.12-beta.2

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 (213) hide show
  1. package/docs/advanced.md +351 -0
  2. package/docs/ai.md +248 -0
  3. package/docs/api.md +323 -0
  4. package/docs/basics.md +979 -0
  5. package/docs/bdd.md +539 -0
  6. package/docs/best.md +237 -0
  7. package/docs/books.md +37 -0
  8. package/docs/bootstrap.md +135 -0
  9. package/docs/build/ApiDataFactory.js +410 -0
  10. package/docs/build/Appium.js +2027 -0
  11. package/docs/build/Expect.js +422 -0
  12. package/docs/build/FileSystem.js +228 -0
  13. package/docs/build/GraphQL.js +229 -0
  14. package/docs/build/GraphQLDataFactory.js +309 -0
  15. package/docs/build/JSONResponse.js +338 -0
  16. package/docs/build/Mochawesome.js +71 -0
  17. package/docs/build/Nightmare.js +2152 -0
  18. package/docs/build/OpenAI.js +126 -0
  19. package/docs/build/Playwright.js +5110 -0
  20. package/docs/build/Protractor.js +2706 -0
  21. package/docs/build/Puppeteer.js +3905 -0
  22. package/docs/build/REST.js +344 -0
  23. package/docs/build/TestCafe.js +2125 -0
  24. package/docs/build/WebDriver.js +4240 -0
  25. package/docs/changelog.md +2572 -0
  26. package/docs/commands.md +266 -0
  27. package/docs/community-helpers.md +58 -0
  28. package/docs/configuration.md +157 -0
  29. package/docs/continuous-integration.md +22 -0
  30. package/docs/custom-helpers.md +306 -0
  31. package/docs/data.md +379 -0
  32. package/docs/detox.md +235 -0
  33. package/docs/docker.md +136 -0
  34. package/docs/email.md +183 -0
  35. package/docs/examples.md +149 -0
  36. package/docs/helpers/ApiDataFactory.md +266 -0
  37. package/docs/helpers/Appium.md +1374 -0
  38. package/docs/helpers/Detox.md +586 -0
  39. package/docs/helpers/Expect.md +275 -0
  40. package/docs/helpers/FileSystem.md +152 -0
  41. package/docs/helpers/GraphQL.md +151 -0
  42. package/docs/helpers/GraphQLDataFactory.md +226 -0
  43. package/docs/helpers/JSONResponse.md +254 -0
  44. package/docs/helpers/Mochawesome.md +8 -0
  45. package/docs/helpers/MockRequest.md +377 -0
  46. package/docs/helpers/Nightmare.md +1305 -0
  47. package/docs/helpers/OpenAI.md +70 -0
  48. package/docs/helpers/Playwright.md +2759 -0
  49. package/docs/helpers/Polly.md +44 -0
  50. package/docs/helpers/Protractor.md +1769 -0
  51. package/docs/helpers/Puppeteer-firefox.md +86 -0
  52. package/docs/helpers/Puppeteer.md +2317 -0
  53. package/docs/helpers/REST.md +218 -0
  54. package/docs/helpers/TestCafe.md +1321 -0
  55. package/docs/helpers/WebDriver.md +2547 -0
  56. package/docs/hooks.md +340 -0
  57. package/docs/index.md +111 -0
  58. package/docs/installation.md +75 -0
  59. package/docs/internal-api.md +266 -0
  60. package/docs/locators.md +339 -0
  61. package/docs/mobile-react-native-locators.md +67 -0
  62. package/docs/mobile.md +338 -0
  63. package/docs/pageobjects.md +291 -0
  64. package/docs/parallel.md +400 -0
  65. package/docs/playwright.md +632 -0
  66. package/docs/plugins.md +1259 -0
  67. package/docs/puppeteer.md +316 -0
  68. package/docs/quickstart.md +162 -0
  69. package/docs/react.md +70 -0
  70. package/docs/reports.md +392 -0
  71. package/docs/secrets.md +36 -0
  72. package/docs/shadow.md +68 -0
  73. package/docs/shared/keys.mustache +31 -0
  74. package/docs/shared/react.mustache +1 -0
  75. package/docs/testcafe.md +174 -0
  76. package/docs/translation.md +247 -0
  77. package/docs/tutorial.md +271 -0
  78. package/docs/typescript.md +180 -0
  79. package/docs/ui.md +59 -0
  80. package/docs/videos.md +28 -0
  81. package/docs/visual.md +202 -0
  82. package/docs/vue.md +143 -0
  83. package/docs/webapi/amOnPage.mustache +11 -0
  84. package/docs/webapi/appendField.mustache +11 -0
  85. package/docs/webapi/attachFile.mustache +12 -0
  86. package/docs/webapi/blur.mustache +18 -0
  87. package/docs/webapi/checkOption.mustache +13 -0
  88. package/docs/webapi/clearCookie.mustache +9 -0
  89. package/docs/webapi/clearField.mustache +9 -0
  90. package/docs/webapi/click.mustache +25 -0
  91. package/docs/webapi/clickLink.mustache +8 -0
  92. package/docs/webapi/closeCurrentTab.mustache +7 -0
  93. package/docs/webapi/closeOtherTabs.mustache +8 -0
  94. package/docs/webapi/dontSee.mustache +11 -0
  95. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +10 -0
  96. package/docs/webapi/dontSeeCookie.mustache +8 -0
  97. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +10 -0
  98. package/docs/webapi/dontSeeElement.mustache +8 -0
  99. package/docs/webapi/dontSeeElementInDOM.mustache +8 -0
  100. package/docs/webapi/dontSeeInCurrentUrl.mustache +4 -0
  101. package/docs/webapi/dontSeeInField.mustache +11 -0
  102. package/docs/webapi/dontSeeInSource.mustache +8 -0
  103. package/docs/webapi/dontSeeInTitle.mustache +8 -0
  104. package/docs/webapi/doubleClick.mustache +13 -0
  105. package/docs/webapi/downloadFile.mustache +12 -0
  106. package/docs/webapi/dragAndDrop.mustache +9 -0
  107. package/docs/webapi/dragSlider.mustache +11 -0
  108. package/docs/webapi/executeAsyncScript.mustache +24 -0
  109. package/docs/webapi/executeScript.mustache +26 -0
  110. package/docs/webapi/fillField.mustache +16 -0
  111. package/docs/webapi/focus.mustache +13 -0
  112. package/docs/webapi/forceClick.mustache +28 -0
  113. package/docs/webapi/forceRightClick.mustache +18 -0
  114. package/docs/webapi/grabAllWindowHandles.mustache +7 -0
  115. package/docs/webapi/grabAttributeFrom.mustache +10 -0
  116. package/docs/webapi/grabAttributeFromAll.mustache +9 -0
  117. package/docs/webapi/grabBrowserLogs.mustache +9 -0
  118. package/docs/webapi/grabCookie.mustache +11 -0
  119. package/docs/webapi/grabCssPropertyFrom.mustache +11 -0
  120. package/docs/webapi/grabCssPropertyFromAll.mustache +10 -0
  121. package/docs/webapi/grabCurrentUrl.mustache +9 -0
  122. package/docs/webapi/grabCurrentWindowHandle.mustache +6 -0
  123. package/docs/webapi/grabDataFromPerformanceTiming.mustache +20 -0
  124. package/docs/webapi/grabElementBoundingRect.mustache +20 -0
  125. package/docs/webapi/grabGeoLocation.mustache +8 -0
  126. package/docs/webapi/grabHTMLFrom.mustache +10 -0
  127. package/docs/webapi/grabHTMLFromAll.mustache +9 -0
  128. package/docs/webapi/grabNumberOfOpenTabs.mustache +8 -0
  129. package/docs/webapi/grabNumberOfVisibleElements.mustache +9 -0
  130. package/docs/webapi/grabPageScrollPosition.mustache +8 -0
  131. package/docs/webapi/grabPopupText.mustache +5 -0
  132. package/docs/webapi/grabSource.mustache +8 -0
  133. package/docs/webapi/grabTextFrom.mustache +10 -0
  134. package/docs/webapi/grabTextFromAll.mustache +9 -0
  135. package/docs/webapi/grabTitle.mustache +8 -0
  136. package/docs/webapi/grabValueFrom.mustache +9 -0
  137. package/docs/webapi/grabValueFromAll.mustache +8 -0
  138. package/docs/webapi/grabWebElement.mustache +9 -0
  139. package/docs/webapi/grabWebElements.mustache +9 -0
  140. package/docs/webapi/moveCursorTo.mustache +12 -0
  141. package/docs/webapi/openNewTab.mustache +7 -0
  142. package/docs/webapi/pressKey.mustache +12 -0
  143. package/docs/webapi/pressKeyDown.mustache +12 -0
  144. package/docs/webapi/pressKeyUp.mustache +12 -0
  145. package/docs/webapi/pressKeyWithKeyNormalization.mustache +60 -0
  146. package/docs/webapi/refreshPage.mustache +6 -0
  147. package/docs/webapi/resizeWindow.mustache +6 -0
  148. package/docs/webapi/rightClick.mustache +14 -0
  149. package/docs/webapi/saveElementScreenshot.mustache +10 -0
  150. package/docs/webapi/saveScreenshot.mustache +12 -0
  151. package/docs/webapi/say.mustache +10 -0
  152. package/docs/webapi/scrollIntoView.mustache +11 -0
  153. package/docs/webapi/scrollPageToBottom.mustache +6 -0
  154. package/docs/webapi/scrollPageToTop.mustache +6 -0
  155. package/docs/webapi/scrollTo.mustache +12 -0
  156. package/docs/webapi/see.mustache +11 -0
  157. package/docs/webapi/seeAttributesOnElements.mustache +9 -0
  158. package/docs/webapi/seeCheckboxIsChecked.mustache +10 -0
  159. package/docs/webapi/seeCookie.mustache +8 -0
  160. package/docs/webapi/seeCssPropertiesOnElements.mustache +9 -0
  161. package/docs/webapi/seeCurrentUrlEquals.mustache +11 -0
  162. package/docs/webapi/seeElement.mustache +8 -0
  163. package/docs/webapi/seeElementInDOM.mustache +8 -0
  164. package/docs/webapi/seeInCurrentUrl.mustache +8 -0
  165. package/docs/webapi/seeInField.mustache +12 -0
  166. package/docs/webapi/seeInPopup.mustache +8 -0
  167. package/docs/webapi/seeInSource.mustache +7 -0
  168. package/docs/webapi/seeInTitle.mustache +8 -0
  169. package/docs/webapi/seeNumberOfElements.mustache +11 -0
  170. package/docs/webapi/seeNumberOfVisibleElements.mustache +10 -0
  171. package/docs/webapi/seeTextEquals.mustache +9 -0
  172. package/docs/webapi/seeTitleEquals.mustache +8 -0
  173. package/docs/webapi/selectOption.mustache +21 -0
  174. package/docs/webapi/setCookie.mustache +16 -0
  175. package/docs/webapi/setGeoLocation.mustache +12 -0
  176. package/docs/webapi/switchTo.mustache +9 -0
  177. package/docs/webapi/switchToNextTab.mustache +10 -0
  178. package/docs/webapi/switchToPreviousTab.mustache +10 -0
  179. package/docs/webapi/type.mustache +21 -0
  180. package/docs/webapi/uncheckOption.mustache +13 -0
  181. package/docs/webapi/wait.mustache +8 -0
  182. package/docs/webapi/waitForClickable.mustache +11 -0
  183. package/docs/webapi/waitForDetached.mustache +10 -0
  184. package/docs/webapi/waitForElement.mustache +11 -0
  185. package/docs/webapi/waitForEnabled.mustache +6 -0
  186. package/docs/webapi/waitForFunction.mustache +17 -0
  187. package/docs/webapi/waitForInvisible.mustache +10 -0
  188. package/docs/webapi/waitForNumberOfTabs.mustache +9 -0
  189. package/docs/webapi/waitForText.mustache +13 -0
  190. package/docs/webapi/waitForValue.mustache +10 -0
  191. package/docs/webapi/waitForVisible.mustache +10 -0
  192. package/docs/webapi/waitInUrl.mustache +9 -0
  193. package/docs/webapi/waitNumberOfVisibleElements.mustache +10 -0
  194. package/docs/webapi/waitToHide.mustache +10 -0
  195. package/docs/webapi/waitUrlEquals.mustache +10 -0
  196. package/docs/webdriver.md +701 -0
  197. package/docs/wiki/Books-&-Posts.md +27 -0
  198. package/docs/wiki/Community-Helpers-&-Plugins.md +53 -0
  199. package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +61 -0
  200. package/docs/wiki/Examples.md +145 -0
  201. package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +68 -0
  202. package/docs/wiki/Home.md +16 -0
  203. package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
  204. package/docs/wiki/Release-Process.md +24 -0
  205. package/docs/wiki/Roadmap.md +23 -0
  206. package/docs/wiki/Tests.md +1393 -0
  207. package/docs/wiki/Upgrading-to-CodeceptJS-3.md +153 -0
  208. package/docs/wiki/Videos.md +19 -0
  209. package/lib/css2xpath/js/css_to_xpath.js +20 -0
  210. package/lib/css2xpath/js/expression.js +23 -0
  211. package/lib/css2xpath/js/renderer.js +239 -0
  212. package/lib/locator.js +15 -1
  213. package/package.json +12 -9
@@ -0,0 +1,2125 @@
1
+ // @ts-nocheck
2
+ const fs = require('fs');
3
+ const assert = require('assert');
4
+ const path = require('path');
5
+ const qrcode = require('qrcode-terminal');
6
+ const createTestCafe = require('testcafe');
7
+ const { Selector, ClientFunction } = require('testcafe');
8
+
9
+ const Helper = require('@codeceptjs/helper');
10
+ const ElementNotFound = require('./errors/ElementNotFound');
11
+ const testControllerHolder = require('./testcafe/testControllerHolder');
12
+ const {
13
+ mapError,
14
+ createTestFile,
15
+ createClientFunction,
16
+ } = require('./testcafe/testcafe-utils');
17
+
18
+ const stringIncludes = require('../assert/include').includes;
19
+ const { urlEquals } = require('../assert/equal');
20
+ const { empty } = require('../assert/empty');
21
+ const { truth } = require('../assert/truth');
22
+ const {
23
+ xpathLocator, normalizeSpacesInString,
24
+ } = require('../utils');
25
+ const Locator = require('../locator');
26
+
27
+ /**
28
+ * Client Functions
29
+ */
30
+ const getPageUrl = t => ClientFunction(() => document.location.href).with({ boundTestRun: t });
31
+ const getHtmlSource = t => ClientFunction(() => document.getElementsByTagName('html')[0].innerHTML).with({ boundTestRun: t });
32
+
33
+ /**
34
+ * Uses [TestCafe](https://github.com/DevExpress/testcafe) library to run cross-browser tests.
35
+ * The browser version you want to use in tests must be installed on your system.
36
+ *
37
+ * Requires `testcafe` package to be installed.
38
+ *
39
+ * ```
40
+ * npm i testcafe --save-dev
41
+ * ```
42
+ *
43
+ * ## Configuration
44
+ *
45
+ * This helper should be configured in codecept.conf.ts or codecept.conf.js
46
+ *
47
+ * * `url`: base url of website to be tested
48
+ * * `show`: (optional, default: false) - show browser window.
49
+ * * `windowSize`: (optional) - set browser window width and height
50
+ * * `getPageTimeout` (optional, default: '30000') config option to set maximum navigation time in milliseconds.
51
+ * * `waitForTimeout`: (optional) default wait* timeout in ms. Default: 5000.
52
+ * * `browser`: (optional, default: chrome) - See https://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html
53
+ *
54
+ *
55
+ * #### Example #1: Show chrome browser window
56
+ *
57
+ * ```js
58
+ * {
59
+ * helpers: {
60
+ * TestCafe : {
61
+ * url: "http://localhost",
62
+ * waitForTimeout: 15000,
63
+ * show: true,
64
+ * browser: "chrome"
65
+ * }
66
+ * }
67
+ * }
68
+ * ```
69
+ *
70
+ * To use remote device you can provide 'remote' as browser parameter this will display a link with QR Code
71
+ * See https://devexpress.github.io/testcafe/documentation/recipes/test-on-remote-computers-and-mobile-devices.html
72
+ * #### Example #2: Remote browser connection
73
+ *
74
+ * ```js
75
+ * {
76
+ * helpers: {
77
+ * TestCafe : {
78
+ * url: "http://localhost",
79
+ * waitForTimeout: 15000,
80
+ * browser: "remote"
81
+ * }
82
+ * }
83
+ * }
84
+ * ```
85
+ *
86
+ * ## Access From Helpers
87
+ *
88
+ * Call Testcafe methods directly using the testcafe controller.
89
+ *
90
+ * ```js
91
+ * const testcafeTestController = this.helpers['TestCafe'].t;
92
+ * const comboBox = Selector('.combo-box');
93
+ * await testcafeTestController
94
+ * .hover(comboBox) // hover over combo box
95
+ * .click('#i-prefer-both') // click some other element
96
+ * ```
97
+ *
98
+ * ## Methods
99
+ */
100
+ class TestCafe extends Helper {
101
+ constructor(config) {
102
+ super(config);
103
+
104
+ this.testcafe = undefined; // testcafe instance
105
+ this.t = undefined; // testcafe test controller
106
+ this.dummyTestcafeFile; // generated testcafe test file
107
+
108
+ // context is used for within() function.
109
+ // It requires to have _withinBeginand _withinEnd implemented.
110
+ // Inside _withinBegin we should define that all next element calls should be started from a specific element (this.context).
111
+ this.context = undefined; // TODO Not sure if this applies to testcafe
112
+
113
+ this.options = {
114
+ url: 'http://localhost',
115
+ show: false,
116
+ browser: 'chrome',
117
+ restart: true, // TODO Test if restart false works
118
+ manualStart: false,
119
+ keepBrowserState: false,
120
+ waitForTimeout: 5000,
121
+ getPageTimeout: 30000,
122
+ fullPageScreenshots: false,
123
+ disableScreenshots: false,
124
+ windowSize: undefined,
125
+ ...config,
126
+ };
127
+ }
128
+
129
+ // TOOD Do a requirements check
130
+ static _checkRequirements() {
131
+ try {
132
+ require('testcafe');
133
+ } catch (e) {
134
+ return ['testcafe@^1.1.0'];
135
+ }
136
+ }
137
+
138
+ static _config() {
139
+ return [
140
+ { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
141
+ { name: 'browser', message: 'Browser to be used', default: 'chrome' },
142
+ {
143
+ name: 'show', message: 'Show browser window', default: true, type: 'confirm',
144
+ },
145
+ ];
146
+ }
147
+
148
+ async _configureAndStartBrowser() {
149
+ this.dummyTestcafeFile = createTestFile(global.output_dir); // create a dummy test file to get hold of the test controller
150
+
151
+ this.iteration += 2; // Use different ports for each test run
152
+ // @ts-ignore
153
+ this.testcafe = await createTestCafe('', null, null);
154
+
155
+ this.debugSection('_before', 'Starting testcafe browser...');
156
+
157
+ this.isRunning = true;
158
+
159
+ // TODO Do we have to cleanup the runner?
160
+ const runner = this.testcafe.createRunner();
161
+
162
+ this.options.browser !== 'remote' ? this._startBrowser(runner) : this._startRemoteBrowser(runner);
163
+
164
+ this.t = await testControllerHolder.get();
165
+ assert(this.t, 'Expected to have the testcafe test controller');
166
+
167
+ if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) {
168
+ const dimensions = this.options.windowSize.split('x');
169
+ await this.t.resizeWindow(parseInt(dimensions[0], 10), parseInt(dimensions[1], 10));
170
+ }
171
+ }
172
+
173
+ async _startBrowser(runner) {
174
+ runner
175
+ .src(this.dummyTestcafeFile)
176
+ .screenshots(global.output_dir, !this.options.disableScreenshots)
177
+ // .video(global.output_dir) // TODO Make this configurable
178
+ .browsers(this.options.show ? this.options.browser : `${this.options.browser}:headless`)
179
+ .reporter('minimal')
180
+ .run({
181
+ skipJsErrors: true,
182
+ skipUncaughtErrors: true,
183
+ quarantineMode: false,
184
+ // debugMode: true,
185
+ // debugOnFail: true,
186
+ // developmentMode: true,
187
+ pageLoadTimeout: this.options.getPageTimeout,
188
+ selectorTimeout: this.options.waitForTimeout,
189
+ assertionTimeout: this.options.waitForTimeout,
190
+ takeScreenshotsOnFails: true,
191
+ })
192
+ .catch((err) => {
193
+ this.debugSection('_before', `Error ${err.toString()}`);
194
+ this.isRunning = false;
195
+ this.testcafe.close();
196
+ });
197
+ }
198
+
199
+ async _startRemoteBrowser(runner) {
200
+ const remoteConnection = await this.testcafe.createBrowserConnection();
201
+ console.log('Connect your device to the following URL or scan QR Code: ', remoteConnection.url);
202
+ qrcode.generate(remoteConnection.url);
203
+ remoteConnection.once('ready', () => {
204
+ runner
205
+ .src(this.dummyTestcafeFile)
206
+ .browsers(remoteConnection)
207
+ .reporter('minimal')
208
+ .run({
209
+ selectorTimeout: this.options.waitForTimeout,
210
+ skipJsErrors: true,
211
+ skipUncaughtErrors: true,
212
+ })
213
+ .catch((err) => {
214
+ this.debugSection('_before', `Error ${err.toString()}`);
215
+ this.isRunning = false;
216
+ this.testcafe.close();
217
+ });
218
+ });
219
+ }
220
+
221
+ async _stopBrowser() {
222
+ this.debugSection('_after', 'Stopping testcafe browser...');
223
+
224
+ testControllerHolder.free();
225
+ if (this.testcafe) {
226
+ this.testcafe.close();
227
+ }
228
+
229
+ fs.unlinkSync(this.dummyTestcafeFile); // remove the dummy test
230
+ this.t = undefined;
231
+
232
+ this.isRunning = false;
233
+ }
234
+
235
+ _init() {
236
+ }
237
+
238
+ async _beforeSuite() {
239
+ if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
240
+ this.debugSection('Session', 'Starting singleton browser session');
241
+ return this._configureAndStartBrowser();
242
+ }
243
+ }
244
+
245
+ async _before() {
246
+ if (this.options.restart && !this.options.manualStart) return this._configureAndStartBrowser();
247
+ if (!this.isRunning && !this.options.manualStart) return this._configureAndStartBrowser();
248
+ this.context = null;
249
+ }
250
+
251
+ async _after() {
252
+ if (!this.isRunning) return;
253
+
254
+ if (this.options.restart) {
255
+ this.isRunning = false;
256
+ return this._stopBrowser();
257
+ }
258
+
259
+ if (this.options.keepBrowserState) return;
260
+
261
+ if (!this.options.keepCookies) {
262
+ this.debugSection('Session', 'cleaning cookies and localStorage');
263
+ await this.clearCookie();
264
+
265
+ // TODO IMHO that should only happen when
266
+ await this.executeScript(() => localStorage.clear())
267
+ .catch((err) => {
268
+ if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
269
+ });
270
+ }
271
+ }
272
+
273
+ _afterSuite() {
274
+ }
275
+
276
+ async _finishTest() {
277
+ if (!this.options.restart && this.isRunning) return this._stopBrowser();
278
+ }
279
+
280
+ /**
281
+ * Use [TestCafe](https://devexpress.github.io/testcafe/documentation/test-api/) API inside a test.
282
+ *
283
+ * First argument is a description of an action.
284
+ * Second argument is async function that gets this helper as parameter.
285
+ *
286
+ * { [`t`](https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller)) } object from TestCafe API is available.
287
+ *
288
+ * ```js
289
+ * I.useTestCafeTo('handle browser dialog', async ({ t }) {
290
+ * await t.setNativeDialogHandler(() => true);
291
+ * });
292
+ * ```
293
+ *
294
+ *
295
+ *
296
+ * @param {string} description used to show in logs.
297
+ * @param {function} fn async functuion that executed with TestCafe helper as argument
298
+ */
299
+ useTestCafeTo(description, fn) {
300
+ return this._useTo(...arguments);
301
+ }
302
+
303
+ /**
304
+ * Get elements by different locator types, including strict locator
305
+ * Should be used in custom helpers:
306
+ *
307
+ * ```js
308
+ * const elements = await this.helpers['TestCafe']._locate('.item');
309
+ * ```
310
+ *
311
+ */
312
+ async _locate(locator) {
313
+ return findElements.call(this, this.context, locator).catch(mapError);
314
+ }
315
+
316
+ async _withinBegin(locator) {
317
+ const els = await this._locate(locator);
318
+ assertElementExists(els, locator);
319
+ this.context = await els.nth(0);
320
+ }
321
+
322
+ async _withinEnd() {
323
+ this.context = null;
324
+ }
325
+
326
+ /**
327
+ * Opens a web page in a browser. Requires relative or absolute url.
328
+ * If url starts with `/`, opens a web page of a site defined in `url` config parameter.
329
+ *
330
+ * ```js
331
+ * I.amOnPage('/'); // opens main page of website
332
+ * I.amOnPage('https://github.com'); // opens github
333
+ * I.amOnPage('/login'); // opens a login page
334
+ * ```
335
+ *
336
+ * @param {string} url url path or global url.
337
+ * @returns {void} automatically synchronized promise through #recorder
338
+ *
339
+ */
340
+ async amOnPage(url) {
341
+ if (!(/^\w+\:\/\//.test(url))) {
342
+ url = this.options.url + url;
343
+ }
344
+
345
+ return this.t.navigateTo(url)
346
+ .catch(mapError);
347
+ }
348
+
349
+ /**
350
+ * Resize the current window to provided width and height.
351
+ * First parameter can be set to `maximize`.
352
+ *
353
+ * @param {number} width width in pixels or `maximize`.
354
+ * @param {number} height height in pixels.
355
+ * @returns {void} automatically synchronized promise through #recorder
356
+ *
357
+ */
358
+ async resizeWindow(width, height) {
359
+ if (width === 'maximize') {
360
+ return this.t.maximizeWindow().catch(mapError);
361
+ }
362
+
363
+ return this.t.resizeWindow(width, height).catch(mapError);
364
+ }
365
+
366
+ /**
367
+ * Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the matching element.
368
+ *
369
+ * Examples:
370
+ *
371
+ * ```js
372
+ * I.dontSee('#add-to-cart-btn');
373
+ * I.focus('#product-tile')
374
+ * I.see('#add-to-cart-bnt');
375
+ * ```
376
+ *
377
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
378
+ * @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-focus) for available options object as 2nd argument.
379
+ * @returns {void} automatically synchronized promise through #recorder
380
+ *
381
+ *
382
+ */
383
+ async focus(locator) {
384
+ const els = await this._locate(locator);
385
+ await assertElementExists(els, locator, 'Element to focus');
386
+ const element = await els.nth(0);
387
+
388
+ const focusElement = ClientFunction(() => element().focus(), { boundTestRun: this.t, dependencies: { element } });
389
+
390
+ return focusElement();
391
+ }
392
+
393
+ /**
394
+ * Remove focus from a text input, button, etc.
395
+ * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.
396
+ *
397
+ * Examples:
398
+ *
399
+ * ```js
400
+ * I.blur('.text-area')
401
+ * ```
402
+ * ```js
403
+ * //element `#product-tile` is focused
404
+ * I.see('#add-to-cart-btn');
405
+ * I.blur('#product-tile')
406
+ * I.dontSee('#add-to-cart-btn');
407
+ * ```
408
+ *
409
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
410
+ * @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument.
411
+ * @returns {void} automatically synchronized promise through #recorder
412
+ *
413
+ *
414
+ */
415
+ async blur(locator) {
416
+ const els = await this._locate(locator);
417
+ await assertElementExists(els, locator, 'Element to blur');
418
+ const element = await els.nth(0);
419
+
420
+ const blurElement = ClientFunction(() => element().blur(), { boundTestRun: this.t, dependencies: { element } });
421
+
422
+ return blurElement();
423
+ }
424
+
425
+ /**
426
+ * Perform a click on a link or a button, given by a locator.
427
+ * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
428
+ * For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
429
+ * For images, the "alt" attribute and inner text of any parent links are searched.
430
+ *
431
+ * The second parameter is a context (CSS or XPath locator) to narrow the search.
432
+ *
433
+ * ```js
434
+ * // simple link
435
+ * I.click('Logout');
436
+ * // button of form
437
+ * I.click('Submit');
438
+ * // CSS button
439
+ * I.click('#form input[type=submit]');
440
+ * // XPath
441
+ * I.click('//form/*[@type=submit]');
442
+ * // link in context
443
+ * I.click('Logout', '#nav');
444
+ * // using strict locator
445
+ * I.click({css: 'nav a.login'});
446
+ * ```
447
+ *
448
+ * @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
449
+ * @param {?CodeceptJS.LocatorOrString | null} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
450
+ * @returns {void} automatically synchronized promise through #recorder
451
+ *
452
+ *
453
+ */
454
+ async click(locator, context = null) {
455
+ return proceedClick.call(this, locator, context);
456
+ }
457
+
458
+ /**
459
+ * Reload the current page.
460
+ *
461
+ * ```js
462
+ * I.refreshPage();
463
+ * ```
464
+ * @returns {void} automatically synchronized promise through #recorder
465
+ *
466
+ */
467
+ async refreshPage() {
468
+ // eslint-disable-next-line no-restricted-globals
469
+ return this.t.eval(() => location.reload(true), { boundTestRun: this.t }).catch(mapError);
470
+ }
471
+
472
+ /**
473
+ * Waits for an element to become visible on a page (by default waits for 1sec).
474
+ * Element can be located by CSS or XPath.
475
+ *
476
+ * ```js
477
+ * I.waitForVisible('#popup');
478
+ * ```
479
+ *
480
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
481
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
482
+ * @returns {void} automatically synchronized promise through #recorder
483
+ *
484
+ *
485
+ */
486
+ async waitForVisible(locator, sec) {
487
+ const timeout = sec ? sec * 1000 : undefined;
488
+
489
+ return (await findElements.call(this, this.context, locator))
490
+ .with({ visibilityCheck: true, timeout })()
491
+ .catch(mapError);
492
+ }
493
+
494
+ /**
495
+ * Fills a text field or textarea, after clearing its value, with the given string.
496
+ * Field is located by name, label, CSS, or XPath.
497
+ *
498
+ * ```js
499
+ * // by label
500
+ * I.fillField('Email', 'hello@world.com');
501
+ * // by name
502
+ * I.fillField('password', secret('123456'));
503
+ * // by CSS
504
+ * I.fillField('form#login input[name=username]', 'John');
505
+ * // or by strict locator
506
+ * I.fillField({css: 'form#login input[name=username]'}, 'John');
507
+ * ```
508
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
509
+ * @param {CodeceptJS.StringOrSecret} value text value to fill.
510
+ * @returns {void} automatically synchronized promise through #recorder
511
+ *
512
+ */
513
+ async fillField(field, value) {
514
+ const els = await findFields.call(this, field);
515
+ assertElementExists(els, field, 'Field');
516
+ const el = await els.nth(0);
517
+ return this.t
518
+ .typeText(el, value.toString(), { replace: true })
519
+ .catch(mapError);
520
+ }
521
+
522
+ /**
523
+ * Clears a `<textarea>` or text `<input>` element's value.
524
+ *
525
+ * ```js
526
+ * I.clearField('Email');
527
+ * I.clearField('user[email]');
528
+ * I.clearField('#email');
529
+ * ```
530
+ * @param {LocatorOrString} editable field located by label|name|CSS|XPath|strict locator.
531
+ * @returns {void} automatically synchronized promise through #recorder.
532
+ *
533
+ */
534
+ async clearField(field) {
535
+ const els = await findFields.call(this, field);
536
+ assertElementExists(els, field, 'Field');
537
+ const el = await els.nth(0);
538
+
539
+ const res = await this.t
540
+ .selectText(el)
541
+ .pressKey('delete');
542
+ return res;
543
+ }
544
+
545
+ /**
546
+ * Appends text to a input field or textarea.
547
+ * Field is located by name, label, CSS or XPath
548
+ *
549
+ * ```js
550
+ * I.appendField('#myTextField', 'appended');
551
+ * // typing secret
552
+ * I.appendField('password', secret('123456'));
553
+ * ```
554
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
555
+ * @param {string} value text value to append.
556
+ * @returns {void} automatically synchronized promise through #recorder
557
+ *
558
+ *
559
+ */
560
+ async appendField(field, value) {
561
+ const els = await findFields.call(this, field);
562
+ assertElementExists(els, field, 'Field');
563
+ const el = await els.nth(0);
564
+
565
+ return this.t
566
+ .typeText(el, value.toString(), { replace: false })
567
+ .catch(mapError);
568
+ }
569
+
570
+ /**
571
+ * Attaches a file to element located by label, name, CSS or XPath
572
+ * Path to file is relative current codecept directory (where codecept.conf.ts or codecept.conf.js is located).
573
+ * File will be uploaded to remote system (if tests are running remotely).
574
+ *
575
+ * ```js
576
+ * I.attachFile('Avatar', 'data/avatar.jpg');
577
+ * I.attachFile('form input[name=avatar]', 'data/avatar.jpg');
578
+ * ```
579
+ *
580
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
581
+ * @param {string} pathToFile local file path relative to codecept.conf.ts or codecept.conf.js config file.
582
+ * @returns {void} automatically synchronized promise through #recorder
583
+ *
584
+ *
585
+ */
586
+ async attachFile(field, pathToFile) {
587
+ const els = await findFields.call(this, field);
588
+ assertElementExists(els, field, 'Field');
589
+ const el = await els.nth(0);
590
+ const file = path.join(global.codecept_dir, pathToFile);
591
+
592
+ return this.t
593
+ .setFilesToUpload(el, [file])
594
+ .catch(mapError);
595
+ }
596
+
597
+ /**
598
+ * Presses a key on a focused element.
599
+ * Special keys like 'Enter', 'Control', [etc](https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value)
600
+ * will be replaced with corresponding unicode.
601
+ * If modifier key is used (Control, Command, Alt, Shift) in array, it will be released afterwards.
602
+ *
603
+ * ```js
604
+ * I.pressKey('Enter');
605
+ * I.pressKey(['Control','a']);
606
+ * ```
607
+ *
608
+ * @param {string|string[]} key key or array of keys to press.
609
+ * @returns {void} automatically synchronized promise through #recorder
610
+ *
611
+ *
612
+ * {{ keys }}
613
+ */
614
+ async pressKey(key) {
615
+ assert(key, 'Expected a sequence of keys or key combinations');
616
+
617
+ return this.t
618
+ .pressKey(key.toLowerCase()) // testcafe keys are lowercase
619
+ .catch(mapError);
620
+ }
621
+
622
+ /**
623
+ * Moves cursor to element matched by locator.
624
+ * Extra shift can be set with offsetX and offsetY options.
625
+ *
626
+ * ```js
627
+ * I.moveCursorTo('.tooltip');
628
+ * I.moveCursorTo('#submit', 5,5);
629
+ * ```
630
+ *
631
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
632
+ * @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
633
+ * @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
634
+ * @returns {void} automatically synchronized promise through #recorder
635
+ *
636
+ *
637
+ */
638
+ async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
639
+ const els = (await findElements.call(this, this.context, locator)).filterVisible();
640
+ await assertElementExists(els, locator);
641
+
642
+ return this.t
643
+ .hover(els.nth(0), { offsetX, offsetY })
644
+ .catch(mapError);
645
+ }
646
+
647
+ /**
648
+ * Performs a double-click on an element matched by link|button|label|CSS or XPath.
649
+ * Context can be specified as second parameter to narrow search.
650
+ *
651
+ * ```js
652
+ * I.doubleClick('Edit');
653
+ * I.doubleClick('Edit', '.actions');
654
+ * I.doubleClick({css: 'button.accept'});
655
+ * I.doubleClick('.btn.edit');
656
+ * ```
657
+ *
658
+ * @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
659
+ * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
660
+ * @returns {void} automatically synchronized promise through #recorder
661
+ *
662
+ *
663
+ */
664
+ async doubleClick(locator, context = null) {
665
+ let matcher;
666
+ if (context) {
667
+ const els = await this._locate(context);
668
+ await assertElementExists(els, context);
669
+ matcher = await els.nth(0);
670
+ }
671
+
672
+ const els = (await findClickable.call(this, matcher, locator)).filterVisible();
673
+ return this.t
674
+ .doubleClick(els.nth(0))
675
+ .catch(mapError);
676
+ }
677
+
678
+ /**
679
+ * Performs right click on a clickable element matched by semantic locator, CSS or XPath.
680
+ *
681
+ * ```js
682
+ * // right click element with id el
683
+ * I.rightClick('#el');
684
+ * // right click link or button with text "Click me"
685
+ * I.rightClick('Click me');
686
+ * // right click button with text "Click me" inside .context
687
+ * I.rightClick('Click me', '.context');
688
+ * ```
689
+ *
690
+ * @param {CodeceptJS.LocatorOrString} locator clickable element located by CSS|XPath|strict locator.
691
+ * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS|XPath|strict locator.
692
+ * @returns {void} automatically synchronized promise through #recorder
693
+ *
694
+ *
695
+ */
696
+ async rightClick(locator, context = null) {
697
+ let matcher;
698
+ if (context) {
699
+ const els = await this._locate(context);
700
+ await assertElementExists(els, context);
701
+ matcher = await els.nth(0);
702
+ }
703
+ const els = (await findClickable.call(this, matcher, locator)).filterVisible();
704
+ assertElementExists(els, locator);
705
+ return this.t
706
+ .rightClick(els.nth(0))
707
+ .catch(mapError);
708
+ }
709
+
710
+ /**
711
+ * Selects a checkbox or radio button.
712
+ * Element is located by label or name or CSS or XPath.
713
+ *
714
+ * The second parameter is a context (CSS or XPath locator) to narrow the search.
715
+ *
716
+ * ```js
717
+ * I.checkOption('#agree');
718
+ * I.checkOption('I Agree to Terms and Conditions');
719
+ * I.checkOption('agree', '//form');
720
+ * ```
721
+ * @param {CodeceptJS.LocatorOrString} field checkbox located by label | name | CSS | XPath | strict locator.
722
+ * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
723
+ * @returns {void} automatically synchronized promise through #recorder
724
+ *
725
+ */
726
+ async checkOption(field, context = null) {
727
+ const el = await findCheckable.call(this, field, context);
728
+
729
+ return this.t
730
+ .click(el)
731
+ .catch(mapError);
732
+ }
733
+
734
+ /**
735
+ * Unselects a checkbox or radio button.
736
+ * Element is located by label or name or CSS or XPath.
737
+ *
738
+ * The second parameter is a context (CSS or XPath locator) to narrow the search.
739
+ *
740
+ * ```js
741
+ * I.uncheckOption('#agree');
742
+ * I.uncheckOption('I Agree to Terms and Conditions');
743
+ * I.uncheckOption('agree', '//form');
744
+ * ```
745
+ * @param {CodeceptJS.LocatorOrString} field checkbox located by label | name | CSS | XPath | strict locator.
746
+ * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
747
+ * @returns {void} automatically synchronized promise through #recorder
748
+ *
749
+ */
750
+ async uncheckOption(field, context = null) {
751
+ const el = await findCheckable.call(this, field, context);
752
+
753
+ if (await el.checked) {
754
+ return this.t
755
+ .click(el)
756
+ .catch(mapError);
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Verifies that the specified checkbox is checked.
762
+ *
763
+ * ```js
764
+ * I.seeCheckboxIsChecked('Agree');
765
+ * I.seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms
766
+ * I.seeCheckboxIsChecked({css: '#signup_form input[type=checkbox]'});
767
+ * ```
768
+ *
769
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
770
+ * @returns {void} automatically synchronized promise through #recorder
771
+ *
772
+ */
773
+ async seeCheckboxIsChecked(field) {
774
+ return proceedIsChecked.call(this, 'assert', field);
775
+ }
776
+
777
+ /**
778
+ * Verifies that the specified checkbox is not checked.
779
+ *
780
+ * ```js
781
+ * I.dontSeeCheckboxIsChecked('#agree'); // located by ID
782
+ * I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
783
+ * I.dontSeeCheckboxIsChecked('agree'); // located by name
784
+ * ```
785
+ *
786
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
787
+ * @returns {void} automatically synchronized promise through #recorder
788
+ *
789
+ */
790
+ async dontSeeCheckboxIsChecked(field) {
791
+ return proceedIsChecked.call(this, 'negate', field);
792
+ }
793
+
794
+ /**
795
+ * Selects an option in a drop-down select.
796
+ * Field is searched by label | name | CSS | XPath.
797
+ * Option is selected by visible text or by value.
798
+ *
799
+ * ```js
800
+ * I.selectOption('Choose Plan', 'Monthly'); // select by label
801
+ * I.selectOption('subscription', 'Monthly'); // match option by text
802
+ * I.selectOption('subscription', '0'); // or by value
803
+ * I.selectOption('//form/select[@name=account]','Premium');
804
+ * I.selectOption('form select[name=account]', 'Premium');
805
+ * I.selectOption({css: 'form select[name=account]'}, 'Premium');
806
+ * ```
807
+ *
808
+ * Provide an array for the second argument to select multiple options.
809
+ *
810
+ * ```js
811
+ * I.selectOption('Which OS do you use?', ['Android', 'iOS']);
812
+ * ```
813
+ * @param {LocatorOrString} select field located by label|name|CSS|XPath|strict locator.
814
+ * @param {string|Array<*>} option visible text or value of option.
815
+ * @returns {void} automatically synchronized promise through #recorder
816
+ *
817
+ */
818
+ async selectOption(select, option) {
819
+ const els = await findFields.call(this, select);
820
+ assertElementExists(els, select, 'Selectable field');
821
+
822
+ const el = await els.filterVisible().nth(0);
823
+
824
+ if ((await el.tagName).toLowerCase() !== 'select') {
825
+ throw new Error('Element is not <select>');
826
+ }
827
+ if (!Array.isArray(option)) option = [option];
828
+
829
+ // TODO As far as I understand the testcafe docs this should do a multi-select
830
+ // but it does not work
831
+ // const clickOpts = { ctrl: option.length > 1 };
832
+ await this.t.click(el).catch(mapError);
833
+
834
+ for (const key of option) {
835
+ const opt = key;
836
+
837
+ let optEl;
838
+ try {
839
+ optEl = el.child('option').withText(opt);
840
+ if (await optEl.count) {
841
+ await this.t.click(optEl).catch(mapError);
842
+ continue;
843
+ }
844
+ // eslint-disable-next-line no-empty
845
+ } catch (err) {
846
+ }
847
+
848
+ try {
849
+ const sel = `[value="${opt}"]`;
850
+ optEl = el.find(sel);
851
+ if (await optEl.count) {
852
+ await this.t.click(optEl).catch(mapError);
853
+ }
854
+ // eslint-disable-next-line no-empty
855
+ } catch (err) {
856
+ }
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Checks that current url contains a provided fragment.
862
+ *
863
+ * ```js
864
+ * I.seeInCurrentUrl('/register'); // we are on registration page
865
+ * ```
866
+ *
867
+ * @param {string} url a fragment to check
868
+ * @returns {void} automatically synchronized promise through #recorder
869
+ *
870
+ */
871
+ async seeInCurrentUrl(url) {
872
+ stringIncludes('url').assert(url, await getPageUrl(this.t)().catch(mapError));
873
+ }
874
+
875
+ /**
876
+ * Checks that current url does not contain a provided fragment.
877
+ *
878
+ * @param {string} url value to check.
879
+ * @returns {void} automatically synchronized promise through #recorder
880
+ *
881
+ */
882
+ async dontSeeInCurrentUrl(url) {
883
+ stringIncludes('url').negate(url, await getPageUrl(this.t)().catch(mapError));
884
+ }
885
+
886
+ /**
887
+ * Checks that current url is equal to provided one.
888
+ * If a relative url provided, a configured url will be prepended to it.
889
+ * So both examples will work:
890
+ *
891
+ * ```js
892
+ * I.seeCurrentUrlEquals('/register');
893
+ * I.seeCurrentUrlEquals('http://my.site.com/register');
894
+ * ```
895
+ *
896
+ * @param {string} url value to check.
897
+ * @returns {void} automatically synchronized promise through #recorder
898
+ *
899
+ */
900
+ async seeCurrentUrlEquals(url) {
901
+ urlEquals(this.options.url).assert(url, await getPageUrl(this.t)().catch(mapError));
902
+ }
903
+
904
+ /**
905
+ * Checks that current url is not equal to provided one.
906
+ * If a relative url provided, a configured url will be prepended to it.
907
+ *
908
+ * ```js
909
+ * I.dontSeeCurrentUrlEquals('/login'); // relative url are ok
910
+ * I.dontSeeCurrentUrlEquals('http://mysite.com/login'); // absolute urls are also ok
911
+ * ```
912
+ *
913
+ * @param {string} url value to check.
914
+ * @returns {void} automatically synchronized promise through #recorder
915
+ *
916
+ */
917
+ async dontSeeCurrentUrlEquals(url) {
918
+ urlEquals(this.options.url).negate(url, await getPageUrl(this.t)().catch(mapError));
919
+ }
920
+
921
+ /**
922
+ * Checks that a page contains a visible text.
923
+ * Use context parameter to narrow down the search.
924
+ *
925
+ * ```js
926
+ * I.see('Welcome'); // text welcome on a page
927
+ * I.see('Welcome', '.content'); // text inside .content div
928
+ * I.see('Register', {css: 'form.register'}); // use strict locator
929
+ * ```
930
+ * @param {string} text expected on page.
931
+ * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text.
932
+ * @returns {void} automatically synchronized promise through #recorder
933
+ *
934
+ *
935
+ */
936
+ async see(text, context = null) {
937
+ let els;
938
+ if (context) {
939
+ els = (await findElements.call(this, this.context, context)).withText(normalizeSpacesInString(text));
940
+ } else {
941
+ els = (await findElements.call(this, this.context, '*')).withText(normalizeSpacesInString(text));
942
+ }
943
+
944
+ return this.t
945
+ .expect(els.filterVisible().count).gt(0, `No element with text "${text}" found`)
946
+ .catch(mapError);
947
+ }
948
+
949
+ /**
950
+ * Opposite to `see`. Checks that a text is not present on a page.
951
+ * Use context parameter to narrow down the search.
952
+ *
953
+ * ```js
954
+ * I.dontSee('Login'); // assume we are already logged in.
955
+ * I.dontSee('Login', '.nav'); // no login inside .nav element
956
+ * ```
957
+ *
958
+ * @param {string} text which is not present.
959
+ * @param {CodeceptJS.LocatorOrString} [context] (optional) element located by CSS|XPath|strict locator in which to perfrom search.
960
+ * @returns {void} automatically synchronized promise through #recorder
961
+ *
962
+ *
963
+ */
964
+ async dontSee(text, context = null) {
965
+ let els;
966
+ if (context) {
967
+ els = (await findElements.call(this, this.context, context)).withText(text);
968
+ } else {
969
+ els = (await findElements.call(this, this.context, 'body')).withText(text);
970
+ }
971
+
972
+ return this.t
973
+ .expect(els.filterVisible().count).eql(0, `Element with text "${text}" can still be seen`)
974
+ .catch(mapError);
975
+ }
976
+
977
+ /**
978
+ * Checks that a given Element is visible
979
+ * Element is located by CSS or XPath.
980
+ *
981
+ * ```js
982
+ * I.seeElement('#modal');
983
+ * ```
984
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
985
+ * @returns {void} automatically synchronized promise through #recorder
986
+ *
987
+ */
988
+ async seeElement(locator) {
989
+ const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists;
990
+ return this.t
991
+ .expect(exists).ok(`No element "${(new Locator(locator))}" found`)
992
+ .catch(mapError);
993
+ }
994
+
995
+ /**
996
+ * Opposite to `seeElement`. Checks that element is not visible (or in DOM)
997
+ *
998
+ * ```js
999
+ * I.dontSeeElement('.modal'); // modal is not shown
1000
+ * ```
1001
+ *
1002
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|Strict locator.
1003
+ * @returns {void} automatically synchronized promise through #recorder
1004
+ *
1005
+ */
1006
+ async dontSeeElement(locator) {
1007
+ const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists;
1008
+ return this.t
1009
+ .expect(exists).notOk(`Element "${(new Locator(locator))}" is still visible`)
1010
+ .catch(mapError);
1011
+ }
1012
+
1013
+ /**
1014
+ * Checks that a given Element is present in the DOM
1015
+ * Element is located by CSS or XPath.
1016
+ *
1017
+ * ```js
1018
+ * I.seeElementInDOM('#modal');
1019
+ * ```
1020
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1021
+ * @returns {void} automatically synchronized promise through #recorder
1022
+ *
1023
+ */
1024
+ async seeElementInDOM(locator) {
1025
+ const exists = (await findElements.call(this, this.context, locator)).exists;
1026
+ return this.t
1027
+ .expect(exists).ok(`No element "${(new Locator(locator))}" found in DOM`)
1028
+ .catch(mapError);
1029
+ }
1030
+
1031
+ /**
1032
+ * Opposite to `seeElementInDOM`. Checks that element is not on page.
1033
+ *
1034
+ * ```js
1035
+ * I.dontSeeElementInDOM('.nav'); // checks that element is not on page visible or not
1036
+ * ```
1037
+ *
1038
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|Strict locator.
1039
+ * @returns {void} automatically synchronized promise through #recorder
1040
+ *
1041
+ */
1042
+ async dontSeeElementInDOM(locator) {
1043
+ const exists = (await findElements.call(this, this.context, locator)).exists;
1044
+ return this.t
1045
+ .expect(exists).notOk(`Element "${(new Locator(locator))}" is still in DOM`)
1046
+ .catch(mapError);
1047
+ }
1048
+
1049
+ /**
1050
+ * Asserts that an element is visible a given number of times.
1051
+ * Element is located by CSS or XPath.
1052
+ *
1053
+ * ```js
1054
+ * I.seeNumberOfVisibleElements('.buttons', 3);
1055
+ * ```
1056
+ *
1057
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1058
+ * @param {number} num number of elements.
1059
+ * @returns {void} automatically synchronized promise through #recorder
1060
+ *
1061
+ *
1062
+ */
1063
+ async seeNumberOfVisibleElements(locator, num) {
1064
+ const count = (await findElements.call(this, this.context, locator)).filterVisible().count;
1065
+ return this.t
1066
+ .expect(count).eql(num)
1067
+ .catch(mapError);
1068
+ }
1069
+
1070
+ /**
1071
+ * Grab number of visible elements by locator.
1072
+ * Resumes test execution, so **should be used inside async function with `await`** operator.
1073
+ *
1074
+ * ```js
1075
+ * let numOfElements = await I.grabNumberOfVisibleElements('p');
1076
+ * ```
1077
+ *
1078
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
1079
+ * @returns {Promise<number>} number of visible elements
1080
+ */
1081
+ async grabNumberOfVisibleElements(locator) {
1082
+ const count = (await findElements.call(this, this.context, locator)).filterVisible().count;
1083
+ return count;
1084
+ }
1085
+
1086
+ /**
1087
+ * Checks that the given input field or textarea equals to given value.
1088
+ * For fuzzy locators, fields are matched by label text, the "name" attribute, CSS, and XPath.
1089
+ *
1090
+ * ```js
1091
+ * I.seeInField('Username', 'davert');
1092
+ * I.seeInField({css: 'form textarea'},'Type your comment here');
1093
+ * I.seeInField('form input[type=hidden]','hidden_value');
1094
+ * I.seeInField('#searchform input','Search');
1095
+ * ```
1096
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
1097
+ * @param {CodeceptJS.StringOrSecret} value value to check.
1098
+ * @returns {void} automatically synchronized promise through #recorder
1099
+ *
1100
+ */
1101
+ async seeInField(field, value) {
1102
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1103
+ // const expectedValue = findElements.call(this, this.context, field).value;
1104
+ const els = await findFields.call(this, field);
1105
+ assertElementExists(els, field, 'Field');
1106
+ const el = await els.nth(0);
1107
+
1108
+ return this.t
1109
+ .expect(await el.value).eql(_value)
1110
+ .catch(mapError);
1111
+ }
1112
+
1113
+ /**
1114
+ * Checks that value of input field or textarea doesn't equal to given value
1115
+ * Opposite to `seeInField`.
1116
+ *
1117
+ * ```js
1118
+ * I.dontSeeInField('email', 'user@user.com'); // field by name
1119
+ * I.dontSeeInField({ css: 'form input.email' }, 'user@user.com'); // field by CSS
1120
+ * ```
1121
+ *
1122
+ * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
1123
+ * @param {CodeceptJS.StringOrSecret} value value to check.
1124
+ * @returns {void} automatically synchronized promise through #recorder
1125
+ *
1126
+ */
1127
+ async dontSeeInField(field, value) {
1128
+ const _value = (typeof value === 'boolean') ? value : value.toString();
1129
+ // const expectedValue = findElements.call(this, this.context, field).value;
1130
+ const els = await findFields.call(this, field);
1131
+ assertElementExists(els, field, 'Field');
1132
+ const el = await els.nth(0);
1133
+
1134
+ return this.t
1135
+ .expect(el.value).notEql(_value)
1136
+ .catch(mapError);
1137
+ }
1138
+
1139
+ /**
1140
+ * Checks that text is equal to provided one.
1141
+ *
1142
+ * ```js
1143
+ * I.seeTextEquals('text', 'h1');
1144
+ * ```
1145
+ */
1146
+ async seeTextEquals(text, context = null) {
1147
+ const expectedText = findElements.call(this, context, undefined).textContent;
1148
+ return this.t
1149
+ .expect(expectedText).eql(text)
1150
+ .catch(mapError);
1151
+ }
1152
+
1153
+ /**
1154
+ * Checks that the current page contains the given string in its raw source code.
1155
+ *
1156
+ * ```js
1157
+ * I.seeInSource('<h1>Green eggs &amp; ham</h1>');
1158
+ * ```
1159
+ * @param {string} text value to check.
1160
+ * @returns {void} automatically synchronized promise through #recorder
1161
+ *
1162
+ */
1163
+ async seeInSource(text) {
1164
+ const source = await getHtmlSource(this.t)();
1165
+ stringIncludes('HTML source of a page').assert(text, source);
1166
+ }
1167
+
1168
+ /**
1169
+ * Checks that the current page does not contains the given string in its raw source code.
1170
+ *
1171
+ * ```js
1172
+ * I.dontSeeInSource('<!--'); // no comments in source
1173
+ * ```
1174
+ *
1175
+ * @param {string} value to check.
1176
+ * @returns {void} automatically synchronized promise through #recorder
1177
+ *
1178
+ */
1179
+ async dontSeeInSource(text) {
1180
+ const source = await getHtmlSource(this.t)();
1181
+ stringIncludes('HTML source of a page').negate(text, source);
1182
+ }
1183
+
1184
+ /**
1185
+ * Saves screenshot of the specified locator to ouput folder (set in codecept.conf.ts or codecept.conf.js).
1186
+ * Filename is relative to output folder.
1187
+ *
1188
+ * ```js
1189
+ * I.saveElementScreenshot(`#submit`,'debug.png');
1190
+ * ```
1191
+ *
1192
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1193
+ * @param {string} fileName file name to save.
1194
+ * @returns {void} automatically synchronized promise through #recorder
1195
+ *
1196
+ *
1197
+ */
1198
+ async saveElementScreenshot(locator, fileName) {
1199
+ const outputFile = path.join(global.output_dir, fileName);
1200
+
1201
+ const sel = await findElements.call(this, this.context, locator);
1202
+ assertElementExists(sel, locator);
1203
+ const firstElement = await sel.filterVisible().nth(0);
1204
+
1205
+ this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
1206
+ return this.t.takeElementScreenshot(firstElement, fileName);
1207
+ }
1208
+
1209
+ /**
1210
+ * Saves a screenshot to ouput folder (set in codecept.conf.ts or codecept.conf.js).
1211
+ * Filename is relative to output folder.
1212
+ * Optionally resize the window to the full available page `scrollHeight` and `scrollWidth` to capture the entire page by passing `true` in as the second argument.
1213
+ *
1214
+ * ```js
1215
+ * I.saveScreenshot('debug.png');
1216
+ * I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scrollWidth before taking screenshot
1217
+ * ```
1218
+ *
1219
+ * @param {string} fileName file name to save.
1220
+ * @param {boolean} [fullPage=false] (optional, `false` by default) flag to enable fullscreen screenshot mode.
1221
+ * @returns {void} automatically synchronized promise through #recorder
1222
+ *
1223
+ */
1224
+ // TODO Implement full page screenshots
1225
+ async saveScreenshot(fileName) {
1226
+ const outputFile = path.join(global.output_dir, fileName);
1227
+ this.debug(`Screenshot is saving to ${outputFile}`);
1228
+
1229
+ // TODO testcafe automatically creates thumbnail images (which cant be turned off)
1230
+ return this.t.takeScreenshot(fileName);
1231
+ }
1232
+
1233
+ /**
1234
+ * Pauses execution for a number of seconds.
1235
+ *
1236
+ * ```js
1237
+ * I.wait(2); // wait 2 secs
1238
+ * ```
1239
+ *
1240
+ * @param {number} sec number of second to wait.
1241
+ * @returns {void} automatically synchronized promise through #recorder
1242
+ *
1243
+ */
1244
+ async wait(sec) {
1245
+ return new Promise(((done) => {
1246
+ setTimeout(done, sec * 1000);
1247
+ }));
1248
+ }
1249
+
1250
+ /**
1251
+ * Executes sync script on a page.
1252
+ * Pass arguments to function as additional parameters.
1253
+ * Will return execution result to a test.
1254
+ * In this case you should use async function and await to receive results.
1255
+ *
1256
+ * Example with jQuery DatePicker:
1257
+ *
1258
+ * ```js
1259
+ * // change date of jQuery DatePicker
1260
+ * I.executeScript(function() {
1261
+ * // now we are inside browser context
1262
+ * $('date').datetimepicker('setDate', new Date());
1263
+ * });
1264
+ * ```
1265
+ * Can return values. Don't forget to use `await` to get them.
1266
+ *
1267
+ * ```js
1268
+ * let date = await I.executeScript(function(el) {
1269
+ * // only basic types can be returned
1270
+ * return $(el).datetimepicker('getDate').toString();
1271
+ * }, '#date'); // passing jquery selector
1272
+ * ```
1273
+ *
1274
+ * @param {string|function} fn function to be executed in browser context.
1275
+ * @param {...any} args to be passed to function.
1276
+ * @returns {Promise<any>} script return value
1277
+ *
1278
+ *
1279
+ * If a function returns a Promise It will wait for its resolution.
1280
+ */
1281
+ async executeScript(fn, ...args) {
1282
+ const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
1283
+ return browserFn();
1284
+ }
1285
+
1286
+ /**
1287
+ * Retrieves all texts from an element located by CSS or XPath and returns it to test.
1288
+ * Resumes test execution, so **should be used inside async with `await`** operator.
1289
+ *
1290
+ * ```js
1291
+ * let pins = await I.grabTextFromAll('#pin li');
1292
+ * ```
1293
+ *
1294
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1295
+ * @returns {Promise<string[]>} attribute value
1296
+ *
1297
+ */
1298
+ async grabTextFromAll(locator) {
1299
+ const sel = await findElements.call(this, this.context, locator);
1300
+ const length = await sel.count;
1301
+ const texts = [];
1302
+ for (let i = 0; i < length; i++) {
1303
+ texts.push(await sel.nth(i).innerText);
1304
+ }
1305
+
1306
+ return texts;
1307
+ }
1308
+
1309
+ /**
1310
+ * Retrieves a text from an element located by CSS or XPath and returns it to test.
1311
+ * Resumes test execution, so **should be used inside async with `await`** operator.
1312
+ *
1313
+ * ```js
1314
+ * let pin = await I.grabTextFrom('#pin');
1315
+ * ```
1316
+ * If multiple elements found returns first element.
1317
+ *
1318
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1319
+ * @returns {Promise<string>} attribute value
1320
+ *
1321
+ */
1322
+ async grabTextFrom(locator) {
1323
+ const sel = await findElements.call(this, this.context, locator);
1324
+ assertElementExists(sel, locator);
1325
+ const texts = await this.grabTextFromAll(locator);
1326
+ if (texts.length > 1) {
1327
+ this.debugSection('GrabText', `Using first element out of ${texts.length}`);
1328
+ }
1329
+
1330
+ return texts[0];
1331
+ }
1332
+
1333
+ /**
1334
+ * Retrieves an attribute from an element located by CSS or XPath and returns it to test.
1335
+ * Resumes test execution, so **should be used inside async with `await`** operator.
1336
+ * If more than one element is found - attribute of first element is returned.
1337
+ *
1338
+ * ```js
1339
+ * let hint = await I.grabAttributeFrom('#tooltip', 'title');
1340
+ * ```
1341
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1342
+ * @param {string} attr attribute name.
1343
+ * @returns {Promise<string>} attribute value
1344
+ *
1345
+ */
1346
+ async grabAttributeFromAll(locator, attr) {
1347
+ const sel = await findElements.call(this, this.context, locator);
1348
+ const length = await sel.count;
1349
+ const attrs = [];
1350
+ for (let i = 0; i < length; i++) {
1351
+ attrs.push(await (await sel.nth(i)).getAttribute(attr));
1352
+ }
1353
+
1354
+ return attrs;
1355
+ }
1356
+
1357
+ /**
1358
+ * Retrieves an attribute from an element located by CSS or XPath and returns it to test.
1359
+ * Resumes test execution, so **should be used inside async with `await`** operator.
1360
+ * If more than one element is found - attribute of first element is returned.
1361
+ *
1362
+ * ```js
1363
+ * let hint = await I.grabAttributeFrom('#tooltip', 'title');
1364
+ * ```
1365
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1366
+ * @param {string} attr attribute name.
1367
+ * @returns {Promise<string>} attribute value
1368
+ *
1369
+ */
1370
+ async grabAttributeFrom(locator, attr) {
1371
+ const sel = await findElements.call(this, this.context, locator);
1372
+ assertElementExists(sel, locator);
1373
+ const attrs = await this.grabAttributeFromAll(locator, attr);
1374
+ if (attrs.length > 1) {
1375
+ this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`);
1376
+ }
1377
+
1378
+ return attrs[0];
1379
+ }
1380
+
1381
+ /**
1382
+ * Retrieves an array of value from a form located by CSS or XPath and returns it to test.
1383
+ * Resumes test execution, so **should be used inside async function with `await`** operator.
1384
+ *
1385
+ * ```js
1386
+ * let inputs = await I.grabValueFromAll('//form/input');
1387
+ * ```
1388
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1389
+ * @returns {Promise<string[]>} attribute value
1390
+ *
1391
+ */
1392
+ async grabValueFromAll(locator) {
1393
+ const sel = await findElements.call(this, this.context, locator);
1394
+ const length = await sel.count;
1395
+ const values = [];
1396
+ for (let i = 0; i < length; i++) {
1397
+ values.push(await (await sel.nth(i)).value);
1398
+ }
1399
+
1400
+ return values;
1401
+ }
1402
+
1403
+ /**
1404
+ * Retrieves a value from a form element located by CSS or XPath and returns it to test.
1405
+ * Resumes test execution, so **should be used inside async function with `await`** operator.
1406
+ * If more than one element is found - value of first element is returned.
1407
+ *
1408
+ * ```js
1409
+ * let email = await I.grabValueFrom('input[name=email]');
1410
+ * ```
1411
+ * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1412
+ * @returns {Promise<string>} attribute value
1413
+ *
1414
+ */
1415
+ async grabValueFrom(locator) {
1416
+ const sel = await findElements.call(this, this.context, locator);
1417
+ assertElementExists(sel, locator);
1418
+ const values = await this.grabValueFromAll(locator);
1419
+ if (values.length > 1) {
1420
+ this.debugSection('GrabValue', `Using first element out of ${values.length}`);
1421
+ }
1422
+
1423
+ return values[0];
1424
+ }
1425
+
1426
+ /**
1427
+ * Retrieves page source and returns it to test.
1428
+ * Resumes test execution, so **should be used inside async function with `await`** operator.
1429
+ *
1430
+ * ```js
1431
+ * let pageSource = await I.grabSource();
1432
+ * ```
1433
+ *
1434
+ * @returns {Promise<string>} source code
1435
+ */
1436
+ async grabSource() {
1437
+ return ClientFunction(() => document.documentElement.innerHTML).with({ boundTestRun: this.t })();
1438
+ }
1439
+
1440
+ /**
1441
+ * Get JS log from browser.
1442
+ *
1443
+ * ```js
1444
+ * let logs = await I.grabBrowserLogs();
1445
+ * console.log(JSON.stringify(logs))
1446
+ * ```
1447
+ */
1448
+ async grabBrowserLogs() {
1449
+ // TODO Must map?
1450
+ return this.t.getBrowserConsoleMessages();
1451
+ }
1452
+
1453
+ /**
1454
+ * Get current URL from browser.
1455
+ * Resumes test execution, so should be used inside an async function.
1456
+ *
1457
+ * ```js
1458
+ * let url = await I.grabCurrentUrl();
1459
+ * console.log(`Current URL is [${url}]`);
1460
+ * ```
1461
+ *
1462
+ * @returns {Promise<string>} current URL
1463
+ */
1464
+ async grabCurrentUrl() {
1465
+ return ClientFunction(() => document.location.href).with({ boundTestRun: this.t })();
1466
+ }
1467
+
1468
+ /**
1469
+ * Retrieves a page scroll position and returns it to test.
1470
+ * Resumes test execution, so **should be used inside an async function with `await`** operator.
1471
+ *
1472
+ * ```js
1473
+ * let { x, y } = await I.grabPageScrollPosition();
1474
+ * ```
1475
+ *
1476
+ * @returns {Promise<PageScrollPosition>} scroll position
1477
+ *
1478
+ */
1479
+ async grabPageScrollPosition() {
1480
+ return ClientFunction(() => ({ x: window.pageXOffset, y: window.pageYOffset })).with({ boundTestRun: this.t })();
1481
+ }
1482
+
1483
+ /**
1484
+ * Scroll page to the top.
1485
+ *
1486
+ * ```js
1487
+ * I.scrollPageToTop();
1488
+ * ```
1489
+ * @returns {void} automatically synchronized promise through #recorder
1490
+ *
1491
+ */
1492
+ scrollPageToTop() {
1493
+ return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })().catch(mapError);
1494
+ }
1495
+
1496
+ /**
1497
+ * Scroll page to the bottom.
1498
+ *
1499
+ * ```js
1500
+ * I.scrollPageToBottom();
1501
+ * ```
1502
+ * @returns {void} automatically synchronized promise through #recorder
1503
+ *
1504
+ */
1505
+ scrollPageToBottom() {
1506
+ return ClientFunction(() => {
1507
+ const body = document.body;
1508
+ const html = document.documentElement;
1509
+ window.scrollTo(0, Math.max(
1510
+ body.scrollHeight,
1511
+ body.offsetHeight,
1512
+ html.clientHeight,
1513
+ html.scrollHeight,
1514
+ html.offsetHeight,
1515
+ ));
1516
+ }).with({ boundTestRun: this.t })().catch(mapError);
1517
+ }
1518
+
1519
+ /**
1520
+ * Scrolls to element matched by locator.
1521
+ * Extra shift can be set with offsetX and offsetY options.
1522
+ *
1523
+ * ```js
1524
+ * I.scrollTo('footer');
1525
+ * I.scrollTo('#submit', 5, 5);
1526
+ * ```
1527
+ *
1528
+ * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
1529
+ * @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
1530
+ * @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
1531
+ * @returns {void} automatically synchronized promise through #recorder
1532
+ *
1533
+ */
1534
+ async scrollTo(locator, offsetX = 0, offsetY = 0) {
1535
+ if (typeof locator === 'number' && typeof offsetX === 'number') {
1536
+ offsetY = offsetX;
1537
+ offsetX = locator;
1538
+ locator = null;
1539
+ }
1540
+
1541
+ const scrollBy = ClientFunction((offset) => {
1542
+ if (window && window.scrollBy && offset) {
1543
+ window.scrollBy(offset.x, offset.y);
1544
+ }
1545
+ }).with({ boundTestRun: this.t });
1546
+
1547
+ if (locator) {
1548
+ const els = await this._locate(locator);
1549
+ assertElementExists(els, locator, 'Element');
1550
+ const el = await els.nth(0);
1551
+ const x = (await el.offsetLeft) + offsetX;
1552
+ const y = (await el.offsetTop) + offsetY;
1553
+
1554
+ return scrollBy({ x, y }).catch(mapError);
1555
+ }
1556
+
1557
+ const x = offsetX;
1558
+ const y = offsetY;
1559
+ return scrollBy({ x, y }).catch(mapError);
1560
+ }
1561
+
1562
+ /**
1563
+ * Switches frame or in case of null locator reverts to parent.
1564
+ *
1565
+ * ```js
1566
+ * I.switchTo('iframe'); // switch to first iframe
1567
+ * I.switchTo(); // switch back to main page
1568
+ * ```
1569
+ *
1570
+ * @param {?CodeceptJS.LocatorOrString} [locator=null] (optional, `null` by default) element located by CSS|XPath|strict locator.
1571
+ * @returns {void} automatically synchronized promise through #recorder
1572
+ *
1573
+ */
1574
+ async switchTo(locator) {
1575
+ if (Number.isInteger(locator)) {
1576
+ throw new Error('Not supported switching to iframe by number');
1577
+ }
1578
+
1579
+ if (!locator) {
1580
+ return this.t.switchToMainWindow();
1581
+ }
1582
+
1583
+ const el = await findElements.call(this, this.context, locator);
1584
+ return this.t.switchToIframe(el);
1585
+ }
1586
+
1587
+ // TODO Add url assertions
1588
+
1589
+ /**
1590
+ * Sets cookie(s).
1591
+ *
1592
+ * Can be a single cookie object or an array of cookies:
1593
+ *
1594
+ * ```js
1595
+ * I.setCookie({name: 'auth', value: true});
1596
+ *
1597
+ * // as array
1598
+ * I.setCookie([
1599
+ * {name: 'auth', value: true},
1600
+ * {name: 'agree', value: true}
1601
+ * ]);
1602
+ * ```
1603
+ *
1604
+ * @param {Cookie|Array<Cookie>} cookie a cookie object or array of cookie objects.
1605
+ * @returns {void} automatically synchronized promise through #recorder
1606
+ *
1607
+ */
1608
+ async setCookie(cookie) {
1609
+ if (Array.isArray(cookie)) {
1610
+ throw new Error('cookie array is not supported');
1611
+ }
1612
+
1613
+ cookie.path = cookie.path || '/';
1614
+ // cookie.expires = cookie.expires || (new Date()).toUTCString();
1615
+
1616
+ const setCookie = ClientFunction(() => {
1617
+ document.cookie = `${cookie.name}=${cookie.value};path=${cookie.path};expires=${cookie.expires};`;
1618
+ }, { dependencies: { cookie } }).with({ boundTestRun: this.t });
1619
+
1620
+ return setCookie();
1621
+ }
1622
+
1623
+ /**
1624
+ * Checks that cookie with given name exists.
1625
+ *
1626
+ * ```js
1627
+ * I.seeCookie('Auth');
1628
+ * ```
1629
+ *
1630
+ * @param {string} name cookie name.
1631
+ * @returns {void} automatically synchronized promise through #recorder
1632
+ *
1633
+ *
1634
+ */
1635
+ async seeCookie(name) {
1636
+ const cookie = await this.grabCookie(name);
1637
+ empty(`cookie ${name} to be set`).negate(cookie);
1638
+ }
1639
+
1640
+ /**
1641
+ * Checks that cookie with given name does not exist.
1642
+ *
1643
+ * ```js
1644
+ * I.dontSeeCookie('auth'); // no auth cookie
1645
+ * ```
1646
+ *
1647
+ * @param {string} name cookie name.
1648
+ * @returns {void} automatically synchronized promise through #recorder
1649
+ *
1650
+ */
1651
+ async dontSeeCookie(name) {
1652
+ const cookie = await this.grabCookie(name);
1653
+ empty(`cookie ${name} not to be set`).assert(cookie);
1654
+ }
1655
+
1656
+ /**
1657
+ * Gets a cookie object by name.
1658
+ * If none provided gets all cookies.
1659
+ * Resumes test execution, so **should be used inside async function with `await`** operator.
1660
+ *
1661
+ * ```js
1662
+ * let cookie = await I.grabCookie('auth');
1663
+ * assert(cookie.value, '123456');
1664
+ * ```
1665
+ *
1666
+ * @param {?string} [name=null] cookie name.
1667
+ * @returns {any} attribute value
1668
+ *
1669
+ *
1670
+ * Returns cookie in JSON format. If name not passed returns all cookies for this domain.
1671
+ */
1672
+ async grabCookie(name) {
1673
+ if (!name) {
1674
+ const getCookie = ClientFunction(() => {
1675
+ return document.cookie.split(';').map(c => c.split('='));
1676
+ }).with({ boundTestRun: this.t });
1677
+ const cookies = await getCookie();
1678
+ return cookies.map(cookie => ({ name: cookie[0].trim(), value: cookie[1] }));
1679
+ }
1680
+ const getCookie = ClientFunction(() => {
1681
+ // eslint-disable-next-line prefer-template
1682
+ const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
1683
+ return v ? v[2] : null;
1684
+ }, { dependencies: { name } }).with({ boundTestRun: this.t });
1685
+ const value = await getCookie();
1686
+ if (value) return { name, value };
1687
+ }
1688
+
1689
+ /**
1690
+ * Clears a cookie by name,
1691
+ * if none provided clears all cookies.
1692
+ *
1693
+ * ```js
1694
+ * I.clearCookie();
1695
+ * I.clearCookie('test'); // Playwright currently doesn't support clear a particular cookie name
1696
+ * ```
1697
+ *
1698
+ * @param {?string} [cookie=null] (optional, `null` by default) cookie name
1699
+ *
1700
+ */
1701
+ async clearCookie(cookieName) {
1702
+ const clearCookies = ClientFunction(() => {
1703
+ const cookies = document.cookie.split(';');
1704
+
1705
+ for (let i = 0; i < cookies.length; i++) {
1706
+ const cookie = cookies[i];
1707
+ const eqPos = cookie.indexOf('=');
1708
+ const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
1709
+ if (cookieName === undefined || name === cookieName) {
1710
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`;
1711
+ }
1712
+ }
1713
+ }, { dependencies: { cookieName } }).with({ boundTestRun: this.t });
1714
+
1715
+ return clearCookies();
1716
+ }
1717
+
1718
+ /**
1719
+ * Waiting for the part of the URL to match the expected. Useful for SPA to understand that page was changed.
1720
+ *
1721
+ * ```js
1722
+ * I.waitInUrl('/info', 2);
1723
+ * ```
1724
+ *
1725
+ * @param {string} urlPart value to check.
1726
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1727
+ * @returns {void} automatically synchronized promise through #recorder
1728
+ *
1729
+ */
1730
+ async waitInUrl(urlPart, sec = null) {
1731
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1732
+
1733
+ const clientFn = createClientFunction((urlPart) => {
1734
+ const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
1735
+ return currUrl.indexOf(urlPart) > -1;
1736
+ }, [urlPart]).with({ boundTestRun: this.t });
1737
+
1738
+ return waitForFunction(clientFn, waitTimeout).catch(async () => {
1739
+ const currUrl = await this.grabCurrentUrl();
1740
+ throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
1741
+ });
1742
+ }
1743
+
1744
+ /**
1745
+ * Waits for the entire URL to match the expected
1746
+ *
1747
+ * ```js
1748
+ * I.waitUrlEquals('/info', 2);
1749
+ * I.waitUrlEquals('http://127.0.0.1:8000/info');
1750
+ * ```
1751
+ *
1752
+ * @param {string} urlPart value to check.
1753
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1754
+ * @returns {void} automatically synchronized promise through #recorder
1755
+ *
1756
+ */
1757
+ async waitUrlEquals(urlPart, sec = null) {
1758
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1759
+
1760
+ const baseUrl = this.options.url;
1761
+ if (urlPart.indexOf('http') < 0) {
1762
+ urlPart = baseUrl + urlPart;
1763
+ }
1764
+
1765
+ const clientFn = createClientFunction((urlPart) => {
1766
+ const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
1767
+ return currUrl === urlPart;
1768
+ }, [urlPart]).with({ boundTestRun: this.t });
1769
+
1770
+ return waitForFunction(clientFn, waitTimeout).catch(async () => {
1771
+ const currUrl = await this.grabCurrentUrl();
1772
+ throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
1773
+ });
1774
+ }
1775
+
1776
+ /**
1777
+ * Waits for a function to return true (waits for 1 sec by default).
1778
+ * Running in browser context.
1779
+ *
1780
+ * ```js
1781
+ * I.waitForFunction(fn[, [args[, timeout]])
1782
+ * ```
1783
+ *
1784
+ * ```js
1785
+ * I.waitForFunction(() => window.requests == 0);
1786
+ * I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
1787
+ * I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
1788
+ * ```
1789
+ *
1790
+ * @param {string|function} fn to be executed in browser context.
1791
+ * @param {any[]|number} [argsOrSec] (optional, `1` by default) arguments for function or seconds.
1792
+ * @param {number} [sec] (optional, `1` by default) time in seconds to wait
1793
+ * @returns {void} automatically synchronized promise through #recorder
1794
+ *
1795
+ */
1796
+ async waitForFunction(fn, argsOrSec = null, sec = null) {
1797
+ let args = [];
1798
+ if (argsOrSec) {
1799
+ if (Array.isArray(argsOrSec)) {
1800
+ args = argsOrSec;
1801
+ } else if (typeof argsOrSec === 'number') {
1802
+ sec = argsOrSec;
1803
+ }
1804
+ }
1805
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1806
+
1807
+ const clientFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
1808
+
1809
+ return waitForFunction(clientFn, waitTimeout);
1810
+ }
1811
+
1812
+ /**
1813
+ * Waits for a specified number of elements on the page.
1814
+ *
1815
+ * ```js
1816
+ * I.waitNumberOfVisibleElements('a', 3);
1817
+ * ```
1818
+ *
1819
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1820
+ * @param {number} num number of elements.
1821
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1822
+ * @returns {void} automatically synchronized promise through #recorder
1823
+ *
1824
+ */
1825
+ async waitNumberOfVisibleElements(locator, num, sec) {
1826
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1827
+
1828
+ return this.t
1829
+ .expect(createSelector(locator).with({ boundTestRun: this.t }).filterVisible().count)
1830
+ .eql(num, `The number of elements (${(new Locator(locator))}) is not ${num} after ${sec} sec`, { timeout: waitTimeout })
1831
+ .catch(mapError);
1832
+ }
1833
+
1834
+ /**
1835
+ * Waits for element to be present on page (by default waits for 1sec).
1836
+ * Element can be located by CSS or XPath.
1837
+ *
1838
+ * ```js
1839
+ * I.waitForElement('.btn.continue');
1840
+ * I.waitForElement('.btn.continue', 5); // wait for 5 secs
1841
+ * ```
1842
+ *
1843
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1844
+ * @param {number} [sec] (optional, `1` by default) time in seconds to wait
1845
+ * @returns {void} automatically synchronized promise through #recorder
1846
+ *
1847
+ */
1848
+ async waitForElement(locator, sec) {
1849
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1850
+
1851
+ return this.t
1852
+ .expect(createSelector(locator).with({ boundTestRun: this.t }).exists)
1853
+ .ok({ timeout: waitTimeout });
1854
+ }
1855
+
1856
+ /**
1857
+ * Waits for an element to hide (by default waits for 1sec).
1858
+ * Element can be located by CSS or XPath.
1859
+ *
1860
+ * ```js
1861
+ * I.waitToHide('#popup');
1862
+ * ```
1863
+ *
1864
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1865
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1866
+ * @returns {void} automatically synchronized promise through #recorder
1867
+ *
1868
+ */
1869
+ async waitToHide(locator, sec) {
1870
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1871
+
1872
+ return this.t
1873
+ .expect(createSelector(locator).filterHidden().with({ boundTestRun: this.t }).exists)
1874
+ .notOk({ timeout: waitTimeout });
1875
+ }
1876
+
1877
+ /**
1878
+ * Waits for an element to be removed or become invisible on a page (by default waits for 1sec).
1879
+ * Element can be located by CSS or XPath.
1880
+ *
1881
+ * ```js
1882
+ * I.waitForInvisible('#popup');
1883
+ * ```
1884
+ *
1885
+ * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1886
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1887
+ * @returns {void} automatically synchronized promise through #recorder
1888
+ *
1889
+ */
1890
+ async waitForInvisible(locator, sec) {
1891
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1892
+
1893
+ return this.t
1894
+ .expect(createSelector(locator).filterVisible().with({ boundTestRun: this.t }).exists)
1895
+ .ok({ timeout: waitTimeout });
1896
+ }
1897
+
1898
+ /**
1899
+ * Waits for a text to appear (by default waits for 1sec).
1900
+ * Element can be located by CSS or XPath.
1901
+ * Narrow down search results by providing context.
1902
+ *
1903
+ * ```js
1904
+ * I.waitForText('Thank you, form has been submitted');
1905
+ * I.waitForText('Thank you, form has been submitted', 5, '#modal');
1906
+ * ```
1907
+ *
1908
+ * @param {string }text to wait for.
1909
+ * @param {number} [sec=1] (optional, `1` by default) time in seconds to wait
1910
+ * @param {CodeceptJS.LocatorOrString} [context] (optional) element located by CSS|XPath|strict locator.
1911
+ * @returns {void} automatically synchronized promise through #recorder
1912
+ *
1913
+ *
1914
+ */
1915
+ async waitForText(text, sec = null, context = null) {
1916
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
1917
+
1918
+ let els;
1919
+ if (context) {
1920
+ els = (await findElements.call(this, this.context, context));
1921
+ await this.t
1922
+ .expect(els.exists)
1923
+ .ok(`Context element ${context} not found`, { timeout: waitTimeout });
1924
+ } else {
1925
+ els = (await findElements.call(this, this.context, '*'));
1926
+ }
1927
+
1928
+ return this.t
1929
+ .expect(els.withText(text).filterVisible().exists)
1930
+ .ok(`No element with text "${text}" found in ${context || 'body'}`, { timeout: waitTimeout })
1931
+ .catch(mapError);
1932
+ }
1933
+ }
1934
+
1935
+ async function waitForFunction(browserFn, waitTimeout) {
1936
+ const pause = () => new Promise((done => {
1937
+ setTimeout(done, 50);
1938
+ }));
1939
+
1940
+ const start = Date.now();
1941
+ // eslint-disable-next-line no-constant-condition
1942
+ while (true) {
1943
+ let result;
1944
+ try {
1945
+ result = await browserFn();
1946
+ // eslint-disable-next-line no-empty
1947
+ } catch (err) {
1948
+ throw new Error(`Error running function ${err.toString()}`);
1949
+ }
1950
+
1951
+ if (result) return result;
1952
+
1953
+ const duration = (Date.now() - start);
1954
+ if (duration > waitTimeout) {
1955
+ throw new Error('waitForFunction timed out');
1956
+ }
1957
+ await pause(); // make polling
1958
+ }
1959
+ }
1960
+
1961
+ const createSelector = (locator) => {
1962
+ locator = new Locator(locator, 'css');
1963
+ if (locator.isXPath()) return elementByXPath(locator.value);
1964
+ return Selector(locator.simplify());
1965
+ };
1966
+
1967
+ const elementByXPath = (xpath) => {
1968
+ assert(xpath, 'xpath is required');
1969
+
1970
+ return Selector(() => {
1971
+ const iterator = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
1972
+ const items = [];
1973
+
1974
+ let item = iterator.iterateNext();
1975
+
1976
+ while (item) {
1977
+ items.push(item);
1978
+ item = iterator.iterateNext();
1979
+ }
1980
+
1981
+ return items;
1982
+ }, { dependencies: { xpath } });
1983
+ };
1984
+
1985
+ const assertElementExists = async (res, locator, prefix, suffix) => {
1986
+ if (!res || !(await res.count) || !(await res.nth(0).tagName)) {
1987
+ throw new ElementNotFound(locator, prefix, suffix);
1988
+ }
1989
+ };
1990
+
1991
+ async function findElements(matcher, locator) {
1992
+ if (locator && locator.react) throw new Error('react locators are not yet supported');
1993
+
1994
+ locator = new Locator(locator, 'css');
1995
+
1996
+ if (!locator.isXPath()) {
1997
+ return matcher
1998
+ ? matcher.find(locator.simplify())
1999
+ : Selector(locator.simplify()).with({ timeout: 0, boundTestRun: this.t });
2000
+ }
2001
+
2002
+ if (!matcher) return elementByXPath(locator.value).with({ timeout: 0, boundTestRun: this.t });
2003
+
2004
+ return matcher.find((node, idx, originNode) => {
2005
+ const found = document.evaluate(xpath, originNode, null, 5, null);
2006
+ let current = null;
2007
+ while (current = found.iterateNext()) {
2008
+ if (current === node) return true;
2009
+ }
2010
+ return false;
2011
+ }, { xpath: locator.value });
2012
+ }
2013
+
2014
+ async function proceedClick(locator, context = null) {
2015
+ let matcher;
2016
+
2017
+ if (context) {
2018
+ const els = await this._locate(context);
2019
+ await assertElementExists(els, context);
2020
+ matcher = await els.nth(0);
2021
+ }
2022
+
2023
+ const els = await findClickable.call(this, matcher, locator);
2024
+ if (context) {
2025
+ await assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`);
2026
+ } else {
2027
+ await assertElementExists(els, locator, 'Clickable element');
2028
+ }
2029
+
2030
+ const firstElement = await els.filterVisible().nth(0);
2031
+
2032
+ return this.t
2033
+ .click(firstElement)
2034
+ .catch(mapError);
2035
+ }
2036
+
2037
+ async function findClickable(matcher, locator) {
2038
+ if (locator && locator.react) throw new Error('react locators are not yet supported');
2039
+
2040
+ locator = new Locator(locator);
2041
+ if (!locator.isFuzzy()) return (await findElements.call(this, matcher, locator)).filterVisible();
2042
+
2043
+ let els;
2044
+
2045
+ // try to use native TestCafe locator
2046
+ els = matcher ? matcher.find('a,button') : createSelector('a,button');
2047
+ els = await els.withExactText(locator.value).with({ timeout: 0, boundTestRun: this.t });
2048
+ if (await els.count) return els;
2049
+
2050
+ const literal = xpathLocator.literal(locator.value);
2051
+
2052
+ els = (await findElements.call(this, matcher, Locator.clickable.narrow(literal))).filterVisible();
2053
+ if (await els.count) return els;
2054
+
2055
+ els = (await findElements.call(this, matcher, Locator.clickable.wide(literal))).filterVisible();
2056
+ if (await els.count) return els;
2057
+
2058
+ els = (await findElements.call(this, matcher, Locator.clickable.self(literal))).filterVisible();
2059
+ if (await els.count) return els;
2060
+
2061
+ return findElements.call(this, matcher, locator.value); // by css or xpath
2062
+ }
2063
+
2064
+ async function proceedIsChecked(assertType, option) {
2065
+ const els = await findCheckable.call(this, option);
2066
+ assertElementExists(els, option, 'Checkable');
2067
+
2068
+ const selected = await els.checked;
2069
+
2070
+ return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
2071
+ }
2072
+
2073
+ async function findCheckable(locator, context) {
2074
+ assert(locator, 'locator is required');
2075
+ assert(this.t, 'this.t is required');
2076
+
2077
+ let contextEl = await this.context;
2078
+ if (typeof context === 'string') {
2079
+ contextEl = (await findElements.call(this, contextEl, (new Locator(context, 'css')).simplify())).filterVisible();
2080
+ contextEl = await contextEl.nth(0);
2081
+ }
2082
+
2083
+ const matchedLocator = new Locator(locator);
2084
+ if (!matchedLocator.isFuzzy()) {
2085
+ return (await findElements.call(this, contextEl, matchedLocator.simplify())).filterVisible();
2086
+ }
2087
+
2088
+ const literal = xpathLocator.literal(locator);
2089
+ let els = (await findElements.call(this, contextEl, Locator.checkable.byText(literal))).filterVisible();
2090
+ if (await els.count) {
2091
+ return els;
2092
+ }
2093
+
2094
+ els = (await findElements.call(this, contextEl, Locator.checkable.byName(literal))).filterVisible();
2095
+ if (await els.count) {
2096
+ return els;
2097
+ }
2098
+
2099
+ return (await findElements.call(this, contextEl, locator)).filterVisible();
2100
+ }
2101
+
2102
+ async function findFields(locator) {
2103
+ const matchedLocator = new Locator(locator);
2104
+ if (!matchedLocator.isFuzzy()) {
2105
+ return this._locate(matchedLocator);
2106
+ }
2107
+ const literal = xpathLocator.literal(locator);
2108
+
2109
+ let els = await this._locate({ xpath: Locator.field.labelEquals(literal) });
2110
+ if (await els.count) {
2111
+ return els;
2112
+ }
2113
+
2114
+ els = await this._locate({ xpath: Locator.field.labelContains(literal) });
2115
+ if (await els.count) {
2116
+ return els;
2117
+ }
2118
+ els = await this._locate({ xpath: Locator.field.byName(literal) });
2119
+ if (await els.count) {
2120
+ return els;
2121
+ }
2122
+ return this._locate({ css: locator });
2123
+ }
2124
+
2125
+ module.exports = TestCafe;