codeceptjs 2.1.3 → 2.2.1

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