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.
- package/CHANGELOG.md +125 -37
- package/README.md +15 -22
- package/bin/codecept.js +4 -1
- package/docs/acceptance.md +44 -1
- package/docs/advanced.md +1 -1
- package/docs/angular.md +6 -9
- package/docs/basics.md +388 -75
- package/docs/bdd.md +4 -3
- package/docs/best.md +1 -1
- package/docs/books.md +31 -0
- package/docs/build/Appium.js +215 -176
- package/docs/build/Nightmare.js +618 -489
- package/docs/build/Polly.js +189 -0
- package/docs/build/Protractor.js +747 -608
- package/docs/build/Puppeteer.js +914 -633
- package/docs/build/REST.js +1 -1
- package/docs/build/TestCafe.js +1835 -0
- package/docs/build/WebDriver.js +861 -805
- package/docs/build/WebDriverIO.js +616 -617
- package/docs/changelog.md +410 -316
- package/docs/commands.md +6 -6
- package/docs/community-helpers.md +2 -0
- package/docs/detox.md +235 -0
- package/docs/examples.md +23 -0
- package/docs/helpers/ApiDataFactory.md +11 -10
- package/docs/helpers/Appium.md +130 -61
- package/docs/helpers/Detox.md +579 -0
- package/docs/helpers/FileSystem.md +2 -1
- package/docs/helpers/Mochawesome.md +1 -0
- package/docs/helpers/Nightmare.md +348 -128
- package/docs/helpers/Polly.md +85 -0
- package/docs/helpers/Protractor.md +451 -184
- package/docs/helpers/Puppeteer-firefox.md +55 -0
- package/docs/helpers/Puppeteer.md +619 -183
- package/docs/helpers/REST.md +17 -16
- package/docs/helpers/SeleniumWebdriver.md +9 -8
- package/docs/helpers/TestCafe.md +1168 -0
- package/docs/helpers/WebDriver.md +600 -291
- package/docs/helpers/WebDriverIO.md +393 -278
- package/docs/helpers.md +37 -18
- package/docs/locators.md +2 -0
- package/docs/mobile-react-native-locators.md +64 -0
- package/docs/mobile.md +5 -0
- package/docs/plugins.md +54 -13
- package/docs/puppeteer.md +74 -26
- package/docs/quickstart.md +47 -12
- package/docs/react.md +67 -0
- package/docs/reports.md +1 -1
- package/docs/{webapi/_keys.mustache → shared/keys.mustache} +0 -0
- package/docs/shared/react.mustache +1 -0
- package/docs/testcafe.md +157 -0
- package/docs/videos.md +19 -0
- package/docs/webapi/amOnPage.mustache +1 -1
- package/docs/webapi/appendField.mustache +2 -2
- package/docs/webapi/attachFile.mustache +2 -2
- package/docs/webapi/checkOption.mustache +2 -2
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/clearField.mustache +1 -1
- package/docs/webapi/click.mustache +2 -2
- package/docs/webapi/clickLink.mustache +3 -3
- package/docs/webapi/dontSee.mustache +6 -3
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +7 -1
- package/docs/webapi/dontSeeCookie.mustache +5 -1
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +6 -1
- package/docs/webapi/dontSeeElement.mustache +5 -1
- package/docs/webapi/dontSeeElementInDOM.mustache +5 -1
- package/docs/webapi/dontSeeInCurrentUrl.mustache +1 -1
- package/docs/webapi/dontSeeInField.mustache +7 -2
- package/docs/webapi/dontSeeInSource.mustache +5 -1
- package/docs/webapi/dontSeeInTitle.mustache +5 -1
- package/docs/webapi/doubleClick.mustache +2 -2
- package/docs/webapi/downloadFile.mustache +2 -2
- package/docs/webapi/dragAndDrop.mustache +2 -2
- package/docs/webapi/dragSlider.mustache +2 -2
- package/docs/webapi/executeAsyncScript.mustache +1 -1
- package/docs/webapi/executeScript.mustache +1 -1
- package/docs/webapi/fillField.mustache +2 -2
- package/docs/webapi/grabAttributeFrom.mustache +3 -2
- package/docs/webapi/grabBrowserLogs.mustache +3 -1
- package/docs/webapi/grabCookie.mustache +2 -1
- package/docs/webapi/grabCssPropertyFrom.mustache +3 -2
- package/docs/webapi/grabCurrentUrl.mustache +3 -1
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +19 -0
- package/docs/webapi/grabHTMLFrom.mustache +2 -1
- package/docs/webapi/grabNumberOfOpenTabs.mustache +4 -2
- package/docs/webapi/grabNumberOfVisibleElements.mustache +3 -2
- package/docs/webapi/grabPageScrollPosition.mustache +3 -1
- package/docs/webapi/grabSource.mustache +3 -1
- package/docs/webapi/grabTextFrom.mustache +2 -1
- package/docs/webapi/grabTitle.mustache +3 -1
- package/docs/webapi/grabValueFrom.mustache +2 -1
- package/docs/webapi/moveCursorTo.mustache +3 -3
- package/docs/webapi/pressKey.mustache +1 -1
- package/docs/webapi/resizeWindow.mustache +2 -2
- package/docs/webapi/rightClick.mustache +2 -2
- package/docs/webapi/saveScreenshot.mustache +3 -3
- package/docs/webapi/say.mustache +2 -2
- package/docs/webapi/scrollPageToBottom.mustache +1 -1
- package/docs/webapi/scrollPageToTop.mustache +1 -1
- package/docs/webapi/scrollTo.mustache +3 -3
- package/docs/webapi/see.mustache +2 -2
- package/docs/webapi/seeAttributesOnElements.mustache +3 -3
- package/docs/webapi/seeCheckboxIsChecked.mustache +2 -1
- package/docs/webapi/seeCookie.mustache +1 -1
- package/docs/webapi/seeCssPropertiesOnElements.mustache +2 -2
- package/docs/webapi/seeCurrentUrlEquals.mustache +1 -1
- package/docs/webapi/seeElement.mustache +1 -1
- package/docs/webapi/seeElementInDOM.mustache +1 -1
- package/docs/webapi/seeInCurrentUrl.mustache +1 -1
- package/docs/webapi/seeInField.mustache +2 -2
- package/docs/webapi/seeInSource.mustache +1 -1
- package/docs/webapi/seeInTitle.mustache +5 -1
- package/docs/webapi/seeNumberOfElements.mustache +10 -0
- package/docs/webapi/seeNumberOfVisibleElements.mustache +2 -2
- package/docs/webapi/selectOption.mustache +2 -2
- package/docs/webapi/setCookie.mustache +1 -1
- package/docs/webapi/switchTo.mustache +6 -1
- package/docs/webapi/uncheckOption.mustache +2 -2
- package/docs/webapi/wait.mustache +1 -2
- package/docs/webapi/waitForDetached.mustache +3 -3
- package/docs/webapi/waitForElement.mustache +2 -2
- package/docs/webapi/waitForEnabled.mustache +1 -1
- package/docs/webapi/waitForFunction.mustache +3 -3
- package/docs/webapi/waitForInvisible.mustache +3 -3
- package/docs/webapi/waitForText.mustache +3 -3
- package/docs/webapi/waitForValue.mustache +3 -3
- package/docs/webapi/waitForVisible.mustache +3 -3
- package/docs/webapi/waitInUrl.mustache +2 -2
- package/docs/webapi/waitNumberOfVisibleElements.mustache +3 -3
- package/docs/webapi/waitToHide.mustache +3 -3
- package/docs/webapi/waitUntil.mustache +3 -3
- package/docs/webapi/waitUrlEquals.mustache +2 -2
- package/docs/webdriver.md +453 -0
- package/lib/codecept.js +11 -9
- package/lib/command/definitions.js +183 -30
- package/lib/command/gherkin/snippets.js +29 -9
- package/lib/command/init.js +31 -9
- package/lib/command/run-multiple.js +46 -59
- package/lib/command/utils.js +1 -1
- package/lib/container.js +30 -4
- package/lib/data/dataScenarioConfig.js +18 -0
- package/lib/helper/Appium.js +24 -24
- package/lib/helper/Nightmare.js +81 -84
- package/lib/helper/Polly.js +189 -0
- package/lib/helper/Protractor.js +96 -86
- package/lib/helper/Puppeteer.js +238 -113
- package/lib/helper/REST.js +1 -1
- package/lib/helper/TestCafe.js +1257 -0
- package/lib/helper/WebDriver.js +217 -277
- package/lib/helper/WebDriverIO.js +75 -75
- package/lib/helper/clientscripts/nightmare.js +8 -0
- package/lib/helper/extras/React.js +55 -0
- package/lib/helper/testcafe/testControllerHolder.js +42 -0
- package/lib/helper/testcafe/testcafe-utils.js +63 -0
- package/lib/history.js +39 -0
- package/lib/hooks.js +25 -1
- package/lib/interfaces/gherkin.js +17 -1
- package/lib/interfaces/scenarioConfig.js +2 -2
- package/lib/listener/config.js +3 -3
- package/lib/locator.js +6 -0
- package/lib/pause.js +22 -1
- package/lib/plugin/allure.js +63 -0
- package/lib/plugin/autoLogin.js +65 -16
- package/lib/plugin/puppeteerCoverage.js +6 -1
- package/lib/plugin/stepByStepReport.js +4 -3
- package/lib/scenario.js +23 -17
- package/lib/step.js +5 -2
- package/lib/ui.js +1 -1
- package/lib/utils.js +70 -20
- package/package.json +20 -19
- package/translations/de-DE.js +69 -0
- package/translations/index.js +1 -0
- package/docs/video.md +0 -26
|
@@ -0,0 +1,1257 @@
|
|
|
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
|
+
* {{> amOnPage }}
|
|
264
|
+
*/
|
|
265
|
+
async amOnPage(url) {
|
|
266
|
+
if (!(/^\w+\:\/\//.test(url))) {
|
|
267
|
+
url = this.options.url + url;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.t.navigateTo(url)
|
|
271
|
+
.catch(mapError);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* {{> resizeWindow }}
|
|
277
|
+
*/
|
|
278
|
+
async resizeWindow(width, height) {
|
|
279
|
+
if (width === 'maximize') {
|
|
280
|
+
return this.t.maximizeWindow().catch(mapError);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.t.resizeWindow(width, height).catch(mapError);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* {{> click }}
|
|
288
|
+
*
|
|
289
|
+
*/
|
|
290
|
+
async click(locator, context = null) {
|
|
291
|
+
return proceedClick.call(this, locator, context);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* {{> refreshPage }}
|
|
297
|
+
*/
|
|
298
|
+
async refreshPage() {
|
|
299
|
+
// eslint-disable-next-line no-restricted-globals
|
|
300
|
+
return this.t.eval(() => location.reload(true), { boundTestRun: this.t }).catch(mapError);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* {{> waitForVisible }}
|
|
305
|
+
*
|
|
306
|
+
*/
|
|
307
|
+
async waitForVisible(locator, sec) {
|
|
308
|
+
const timeout = sec ? sec * 1000 : undefined;
|
|
309
|
+
|
|
310
|
+
return (await findElements.call(this, this.context, locator))
|
|
311
|
+
.with({ visibilityCheck: true, timeout })()
|
|
312
|
+
.catch(mapError);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* {{> fillField }}
|
|
317
|
+
*/
|
|
318
|
+
async fillField(field, value) {
|
|
319
|
+
const els = await findFields.call(this, field);
|
|
320
|
+
assertElementExists(els, field, 'Field');
|
|
321
|
+
const el = await els.nth(0);
|
|
322
|
+
return this.t
|
|
323
|
+
.typeText(el, value.toString(), { replace: true })
|
|
324
|
+
.catch(mapError);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* {{> clearField }}
|
|
329
|
+
*/
|
|
330
|
+
async clearField(field) {
|
|
331
|
+
const els = await findFields.call(this, field);
|
|
332
|
+
assertElementExists(els, field, 'Field');
|
|
333
|
+
const el = await els.nth(0);
|
|
334
|
+
|
|
335
|
+
return this.t
|
|
336
|
+
.click(el)
|
|
337
|
+
.pressKey('ctrl+a delete');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* {{> appendField }}
|
|
342
|
+
*
|
|
343
|
+
*/
|
|
344
|
+
async appendField(field, value) {
|
|
345
|
+
const els = await findFields.call(this, field);
|
|
346
|
+
assertElementExists(els, field, 'Field');
|
|
347
|
+
const el = await els.nth(0);
|
|
348
|
+
|
|
349
|
+
return this.t
|
|
350
|
+
.typeText(el, value, { replace: false })
|
|
351
|
+
.catch(mapError);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* {{> appendField }}
|
|
356
|
+
*
|
|
357
|
+
*/
|
|
358
|
+
async attachFile(field, pathToFile) {
|
|
359
|
+
const els = await findFields.call(this, field);
|
|
360
|
+
assertElementExists(els, field, 'Field');
|
|
361
|
+
const el = await els.nth(0);
|
|
362
|
+
const file = path.join(global.codecept_dir, pathToFile);
|
|
363
|
+
|
|
364
|
+
return this.t
|
|
365
|
+
.setFilesToUpload(el, [file])
|
|
366
|
+
.catch(mapError);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* {{> pressKey }}
|
|
371
|
+
*
|
|
372
|
+
* {{ keys }}
|
|
373
|
+
*/
|
|
374
|
+
async pressKey(key) {
|
|
375
|
+
assert(key, 'Expected a sequence of keys or key combinations');
|
|
376
|
+
|
|
377
|
+
return this.t
|
|
378
|
+
.pressKey(key.toLowerCase()) // testcafe keys are lowercase
|
|
379
|
+
.catch(mapError);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* {{> moveCursorTo }}
|
|
384
|
+
*
|
|
385
|
+
*/
|
|
386
|
+
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
387
|
+
const els = (await findElements.call(this, this.context, locator)).filterVisible();
|
|
388
|
+
await assertElementExists(els);
|
|
389
|
+
|
|
390
|
+
return this.t
|
|
391
|
+
.hover(els.nth(0), { offsetX, offsetY })
|
|
392
|
+
.catch(mapError);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* {{> doubleClick }}
|
|
397
|
+
*
|
|
398
|
+
*/
|
|
399
|
+
async doubleClick(locator, context = null) {
|
|
400
|
+
let matcher;
|
|
401
|
+
if (context) {
|
|
402
|
+
const els = await this._locate(context);
|
|
403
|
+
await assertElementExists(els, context);
|
|
404
|
+
matcher = await els.nth(0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const els = (await findClickable.call(this, matcher, locator)).filterVisible();
|
|
408
|
+
return this.t
|
|
409
|
+
.doubleClick(els.nth(0))
|
|
410
|
+
.catch(mapError);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* {{> rightClick }}
|
|
415
|
+
*
|
|
416
|
+
*/
|
|
417
|
+
async rightClick(locator, context = null) {
|
|
418
|
+
let matcher;
|
|
419
|
+
if (context) {
|
|
420
|
+
const els = await this._locate(context);
|
|
421
|
+
await assertElementExists(els, context);
|
|
422
|
+
matcher = await els.nth(0);
|
|
423
|
+
}
|
|
424
|
+
const els = (await findClickable.call(this, matcher, locator)).filterVisible();
|
|
425
|
+
assertElementExists(els);
|
|
426
|
+
return this.t
|
|
427
|
+
.rightClick(els.nth(0))
|
|
428
|
+
.catch(mapError);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* {{> checkOption }}
|
|
433
|
+
*/
|
|
434
|
+
async checkOption(field, context = null) {
|
|
435
|
+
const el = await findCheckable.call(this, field, context);
|
|
436
|
+
|
|
437
|
+
return this.t
|
|
438
|
+
.click(el)
|
|
439
|
+
.catch(mapError);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* {{> uncheckOption }}
|
|
444
|
+
*/
|
|
445
|
+
async uncheckOption(field, context = null) {
|
|
446
|
+
const el = await findCheckable.call(this, field, context);
|
|
447
|
+
|
|
448
|
+
if (await el.checked) {
|
|
449
|
+
return this.t
|
|
450
|
+
.click(el)
|
|
451
|
+
.catch(mapError);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* {{> seeCheckboxIsChecked }}
|
|
457
|
+
*/
|
|
458
|
+
async seeCheckboxIsChecked(field) {
|
|
459
|
+
return proceedIsChecked.call(this, 'assert', field);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* {{> dontSeeCheckboxIsChecked }}
|
|
464
|
+
*/
|
|
465
|
+
async dontSeeCheckboxIsChecked(field) {
|
|
466
|
+
return proceedIsChecked.call(this, 'negate', field);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* {{> selectOption }}
|
|
471
|
+
*/
|
|
472
|
+
async selectOption(select, option) {
|
|
473
|
+
const els = await findFields.call(this, select);
|
|
474
|
+
assertElementExists(els, select, 'Selectable field');
|
|
475
|
+
|
|
476
|
+
const el = await els.filterVisible().nth(0);
|
|
477
|
+
|
|
478
|
+
if ((await el.tagName).toLowerCase() !== 'select') {
|
|
479
|
+
throw new Error('Element is not <select>');
|
|
480
|
+
}
|
|
481
|
+
if (!Array.isArray(option)) option = [option];
|
|
482
|
+
|
|
483
|
+
// TODO As far as I understand the testcafe docs this should do a multi-select
|
|
484
|
+
// but it does not work
|
|
485
|
+
const clickOpts = { ctrl: option.length > 1 };
|
|
486
|
+
await this.t.click(el, clickOpts).catch(mapError);
|
|
487
|
+
|
|
488
|
+
for (const key of option) {
|
|
489
|
+
const opt = key;
|
|
490
|
+
|
|
491
|
+
let optEl;
|
|
492
|
+
try {
|
|
493
|
+
optEl = el.child('option').withText(opt);
|
|
494
|
+
if (await optEl.count) {
|
|
495
|
+
await this.t.click(optEl, clickOpts).catch(mapError);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
// eslint-disable-next-line no-empty
|
|
499
|
+
} catch (err) {}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const sel = `[value="${opt}"]`;
|
|
503
|
+
optEl = el.find(sel);
|
|
504
|
+
if (await optEl.count) {
|
|
505
|
+
await this.t.click(optEl, clickOpts).catch(mapError);
|
|
506
|
+
}
|
|
507
|
+
// eslint-disable-next-line no-empty
|
|
508
|
+
} catch (err) {}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* {{> seeInCurrentUrl }}
|
|
514
|
+
*/
|
|
515
|
+
async seeInCurrentUrl(url) {
|
|
516
|
+
stringIncludes('url').assert(url, await getPageUrl(this.t)().catch(mapError));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* {{> dontSeeInCurrentUrl }}
|
|
521
|
+
*/
|
|
522
|
+
async dontSeeInCurrentUrl(url) {
|
|
523
|
+
stringIncludes('url').negate(url, await getPageUrl(this.t)().catch(mapError));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* {{> seeCurrentUrlEquals }}
|
|
528
|
+
*/
|
|
529
|
+
async seeCurrentUrlEquals(url) {
|
|
530
|
+
urlEquals(this.options.url).assert(url, await getPageUrl(this.t)().catch(mapError));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* {{> dontSeeCurrentUrlEquals }}
|
|
535
|
+
*/
|
|
536
|
+
async dontSeeCurrentUrlEquals(url) {
|
|
537
|
+
urlEquals(this.options.url).negate(url, await getPageUrl(this.t)().catch(mapError));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* {{> see }}
|
|
542
|
+
*
|
|
543
|
+
*/
|
|
544
|
+
async see(text, context = null) {
|
|
545
|
+
let els;
|
|
546
|
+
if (context) {
|
|
547
|
+
els = (await findElements.call(this, this.context, context)).withText(text);
|
|
548
|
+
} else {
|
|
549
|
+
els = (await findElements.call(this, this.context, '*')).withText(text);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return this.t
|
|
553
|
+
.expect(els.filterVisible().count).gt(0, `No element with text "${text}" found`)
|
|
554
|
+
.catch(mapError);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* {{> dontSee }}
|
|
559
|
+
*
|
|
560
|
+
*/
|
|
561
|
+
async dontSee(text, context = null) {
|
|
562
|
+
let els;
|
|
563
|
+
if (context) {
|
|
564
|
+
els = (await findElements.call(this, this.context, context)).withText(text);
|
|
565
|
+
} else {
|
|
566
|
+
els = (await findElements.call(this, this.context, 'body')).withText(text);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return this.t
|
|
570
|
+
.expect(els.filterVisible().count).eql(0, `Element with text "${text}" can still be seen`)
|
|
571
|
+
.catch(mapError);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* {{> seeElement }}
|
|
576
|
+
*/
|
|
577
|
+
async seeElement(locator) {
|
|
578
|
+
const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists;
|
|
579
|
+
return this.t
|
|
580
|
+
.expect(exists).ok(`No element "${locator}" found`)
|
|
581
|
+
.catch(mapError);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* {{> dontSeeElement }}
|
|
586
|
+
*/
|
|
587
|
+
async dontSeeElement(locator) {
|
|
588
|
+
const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists;
|
|
589
|
+
return this.t
|
|
590
|
+
.expect(exists).notOk(`Element "${locator}" is still visible`)
|
|
591
|
+
.catch(mapError);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* {{> seeElementInDOM }}
|
|
596
|
+
*/
|
|
597
|
+
async seeElementInDOM(locator) {
|
|
598
|
+
const exists = (await findElements.call(this, this.context, locator)).exists;
|
|
599
|
+
return this.t
|
|
600
|
+
.expect(exists).ok(`No element "${locator}" found in DOM`)
|
|
601
|
+
.catch(mapError);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* {{> dontSeeElementInDOM }}
|
|
606
|
+
*/
|
|
607
|
+
async dontSeeElementInDOM(locator) {
|
|
608
|
+
const exists = (await findElements.call(this, this.context, locator)).exists;
|
|
609
|
+
return this.t
|
|
610
|
+
.expect(exists).notOk(`Element "${locator}" is still in DOM`)
|
|
611
|
+
.catch(mapError);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* {{> seeNumberOfVisibleElements }}
|
|
616
|
+
*
|
|
617
|
+
*/
|
|
618
|
+
async seeNumberOfVisibleElements(locator, num) {
|
|
619
|
+
const count = (await findElements.call(this, this.context, locator)).filterVisible().count;
|
|
620
|
+
return this.t
|
|
621
|
+
.expect(count).eql(num)
|
|
622
|
+
.catch(mapError);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* {{> grabNumberOfVisibleElements }}
|
|
627
|
+
*/
|
|
628
|
+
async grabNumberOfVisibleElements(locator) {
|
|
629
|
+
const count = (await findElements.call(this, this.context, locator)).filterVisible().count;
|
|
630
|
+
return count;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* {{> seeInField }}
|
|
635
|
+
*/
|
|
636
|
+
async seeInField(field, value) {
|
|
637
|
+
// const expectedValue = findElements.call(this, this.context, field).value;
|
|
638
|
+
const els = await findFields.call(this, field);
|
|
639
|
+
assertElementExists(els, field, 'Field');
|
|
640
|
+
const el = await els.nth(0);
|
|
641
|
+
|
|
642
|
+
return this.t
|
|
643
|
+
.expect(await el.value).eql(value)
|
|
644
|
+
.catch(mapError);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* {{> dontSeeInField }}
|
|
649
|
+
*/
|
|
650
|
+
async dontSeeInField(field, value) {
|
|
651
|
+
// const expectedValue = findElements.call(this, this.context, field).value;
|
|
652
|
+
const els = await findFields.call(this, field);
|
|
653
|
+
assertElementExists(els, field, 'Field');
|
|
654
|
+
const el = await els.nth(0);
|
|
655
|
+
|
|
656
|
+
return this.t
|
|
657
|
+
.expect(el.value).notEql(value)
|
|
658
|
+
.catch(mapError);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Checks that text is equal to provided one.
|
|
663
|
+
*
|
|
664
|
+
* ```js
|
|
665
|
+
* I.seeTextEquals('text', 'h1');
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
async seeTextEquals(text, context = null) {
|
|
669
|
+
const expectedText = findElements.call(this, context, undefined).textContent;
|
|
670
|
+
return this.t
|
|
671
|
+
.expect(expectedText).eql(text)
|
|
672
|
+
.catch(mapError);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* {{> seeInSource }}
|
|
677
|
+
*/
|
|
678
|
+
async seeInSource(text) {
|
|
679
|
+
const source = await getHtmlSource(this.t)();
|
|
680
|
+
stringIncludes('HTML source of a page').assert(text, source);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* {{> dontSeeInSource }}
|
|
685
|
+
*/
|
|
686
|
+
async dontSeeInSource(text) {
|
|
687
|
+
const source = await getHtmlSource(this.t)();
|
|
688
|
+
stringIncludes('HTML source of a page').negate(text, source);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* {{> saveScreenshot }}
|
|
694
|
+
*/
|
|
695
|
+
async saveScreenshot(fileName, fullPage) {
|
|
696
|
+
// TODO Implement full page screenshots
|
|
697
|
+
const fullPageOption = fullPage || this.options.fullPageScreenshots;
|
|
698
|
+
|
|
699
|
+
const outputFile = path.join(global.output_dir, fileName);
|
|
700
|
+
this.debug(`Screenshot is saving to ${outputFile}`);
|
|
701
|
+
|
|
702
|
+
// TODO testcafe automatically creates thumbnail images (which cant be turned off)
|
|
703
|
+
return this.t.takeScreenshot(fileName);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* {{> wait }}
|
|
708
|
+
*/
|
|
709
|
+
async wait(sec) {
|
|
710
|
+
return new Promise(((done) => {
|
|
711
|
+
setTimeout(done, sec * 1000);
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* {{> executeScript }}
|
|
718
|
+
*
|
|
719
|
+
* If a function returns a Promise It will wait for it resolution.
|
|
720
|
+
*/
|
|
721
|
+
async executeScript(fn, ...args) {
|
|
722
|
+
const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t });
|
|
723
|
+
return browserFn();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* {{> grabTextFrom }}
|
|
728
|
+
*/
|
|
729
|
+
async grabTextFrom(locator) {
|
|
730
|
+
const sel = await findElements.call(this, this.context, locator);
|
|
731
|
+
assertElementExists(sel);
|
|
732
|
+
const num = await sel.count;
|
|
733
|
+
if (num) {
|
|
734
|
+
const res = [];
|
|
735
|
+
for (let i = 0; i < num; i++) {
|
|
736
|
+
res.push(await sel.nth(i).innerText);
|
|
737
|
+
}
|
|
738
|
+
return res;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return sel.nth(0).innerText;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* {{> grabAttributeFrom }}
|
|
746
|
+
*/
|
|
747
|
+
async grabAttributeFrom(locator, attr) {
|
|
748
|
+
const sel = await findElements.call(this, this.context, locator);
|
|
749
|
+
assertElementExists(sel);
|
|
750
|
+
return (await sel.nth(0)).value;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* {{> grabValueFrom }}
|
|
755
|
+
*/
|
|
756
|
+
async grabValueFrom(locator) {
|
|
757
|
+
return this.grabAttributeFrom(locator, 'value');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* {{> grabSource }}
|
|
762
|
+
*/
|
|
763
|
+
async grabSource() {
|
|
764
|
+
return ClientFunction(() => document.documentElement.innerHTML).with({ boundTestRun: this.t })();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Get JS log from browser.
|
|
769
|
+
*
|
|
770
|
+
* ```js
|
|
771
|
+
* let logs = await I.grabBrowserLogs();
|
|
772
|
+
* console.log(JSON.stringify(logs))
|
|
773
|
+
* ```
|
|
774
|
+
*/
|
|
775
|
+
async grabBrowserLogs() {
|
|
776
|
+
// TODO Must map?
|
|
777
|
+
return this.t.getBrowserConsoleMessages();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* {{> grabCurrentUrl }}
|
|
782
|
+
*/
|
|
783
|
+
async grabCurrentUrl() {
|
|
784
|
+
return ClientFunction(() => document.location.href).with({ boundTestRun: this.t })();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* {{> grabPageScrollPosition }}
|
|
789
|
+
*/
|
|
790
|
+
async grabPageScrollPosition() {
|
|
791
|
+
return ClientFunction(() => ({ x: window.pageXOffset, y: window.pageYOffset })).with({ boundTestRun: this.t })();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* {{> scrollPageToTop }}
|
|
796
|
+
*/
|
|
797
|
+
scrollPageToTop() {
|
|
798
|
+
return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })().catch(mapError);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* {{> scrollPageToBottom }}
|
|
803
|
+
*/
|
|
804
|
+
scrollPageToBottom() {
|
|
805
|
+
return ClientFunction(() => {
|
|
806
|
+
const body = document.body;
|
|
807
|
+
const html = document.documentElement;
|
|
808
|
+
window.scrollTo(0, Math.max(
|
|
809
|
+
body.scrollHeight, body.offsetHeight,
|
|
810
|
+
html.clientHeight, html.scrollHeight, html.offsetHeight,
|
|
811
|
+
));
|
|
812
|
+
}).with({ boundTestRun: this.t })().catch(mapError);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* {{> scrollTo }}
|
|
817
|
+
*/
|
|
818
|
+
async scrollTo(locator, offsetX = 0, offsetY = 0) {
|
|
819
|
+
if (typeof locator === 'number' && typeof offsetX === 'number') {
|
|
820
|
+
offsetY = offsetX;
|
|
821
|
+
offsetX = locator;
|
|
822
|
+
locator = null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const scrollBy = ClientFunction((offset) => {
|
|
826
|
+
if (window && window.scrollBy && offset) {
|
|
827
|
+
window.scrollBy(offset.x, offset.y);
|
|
828
|
+
}
|
|
829
|
+
}).with({ boundTestRun: this.t });
|
|
830
|
+
|
|
831
|
+
if (locator) {
|
|
832
|
+
const els = await this._locate(locator);
|
|
833
|
+
assertElementExists(els, locator, 'Element');
|
|
834
|
+
const el = await els.nth(0);
|
|
835
|
+
const x = (await el.offsetLeft) + offsetX;
|
|
836
|
+
const y = (await el.offsetTop) + offsetY;
|
|
837
|
+
|
|
838
|
+
return scrollBy({ x, y }).catch(mapError);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const x = offsetX;
|
|
842
|
+
const y = offsetY;
|
|
843
|
+
return scrollBy({ x, y }).catch(mapError);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* {{> switchTo }}
|
|
848
|
+
*/
|
|
849
|
+
async switchTo(locator) {
|
|
850
|
+
if (Number.isInteger(locator)) {
|
|
851
|
+
throw new Error('Not supported switching to iframe by number');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!locator) {
|
|
855
|
+
return this.t.switchToMainWindow();
|
|
856
|
+
}
|
|
857
|
+
return this.t.switchToIframe(findElements.call(this, this.context, locator));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// TODO Add url assertions
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* {{> setCookie }}
|
|
864
|
+
*/
|
|
865
|
+
async setCookie(cookie) {
|
|
866
|
+
if (Array.isArray(cookie)) {
|
|
867
|
+
throw new Error('cookie array is not supported');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
cookie.path = cookie.path || '/';
|
|
871
|
+
// cookie.expires = cookie.expires || (new Date()).toUTCString();
|
|
872
|
+
|
|
873
|
+
const setCookie = ClientFunction(() => {
|
|
874
|
+
document.cookie = `${cookie.name}=${cookie.value};path=${cookie.path};expires=${cookie.expires};`;
|
|
875
|
+
}, { dependencies: { cookie } }).with({ boundTestRun: this.t });
|
|
876
|
+
|
|
877
|
+
return setCookie();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* {{> seeCookie }}
|
|
882
|
+
*
|
|
883
|
+
*/
|
|
884
|
+
async seeCookie(name) {
|
|
885
|
+
const cookie = await this.grabCookie(name);
|
|
886
|
+
empty(`cookie ${name} to be set`).negate(cookie);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* {{> dontSeeCookie }}
|
|
891
|
+
*/
|
|
892
|
+
async dontSeeCookie(name) {
|
|
893
|
+
const cookie = await this.grabCookie(name);
|
|
894
|
+
empty(`cookie ${name} not to be set`).assert(cookie);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* {{> grabCookie }}
|
|
899
|
+
*
|
|
900
|
+
* Returns cookie in JSON format. If name not passed returns all cookies for this domain.
|
|
901
|
+
*/
|
|
902
|
+
async grabCookie(name) {
|
|
903
|
+
if (!name) {
|
|
904
|
+
const getCookie = ClientFunction(() => {
|
|
905
|
+
return document.cookie.split(';').map(c => c.split('='));
|
|
906
|
+
}).with({ boundTestRun: this.t });
|
|
907
|
+
const cookies = await getCookie();
|
|
908
|
+
return cookies.map(cookie => ({ name: cookie[0].trim(), value: cookie[1] }));
|
|
909
|
+
}
|
|
910
|
+
const getCookie = ClientFunction(() => {
|
|
911
|
+
// eslint-disable-next-line prefer-template
|
|
912
|
+
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
|
|
913
|
+
return v ? v[2] : null;
|
|
914
|
+
}, { dependencies: { name } }).with({ boundTestRun: this.t });
|
|
915
|
+
const value = await getCookie();
|
|
916
|
+
if (value) return { name, value };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* {{> clearCookie }}
|
|
921
|
+
*/
|
|
922
|
+
async clearCookie(cookieName) {
|
|
923
|
+
const clearCookies = ClientFunction(() => {
|
|
924
|
+
const cookies = document.cookie.split(';');
|
|
925
|
+
|
|
926
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
927
|
+
const cookie = cookies[i];
|
|
928
|
+
const eqPos = cookie.indexOf('=');
|
|
929
|
+
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
|
930
|
+
if (cookieName === undefined || name === cookieName) {
|
|
931
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}, { dependencies: { cookieName } }).with({ boundTestRun: this.t });
|
|
935
|
+
|
|
936
|
+
return clearCookies();
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* {{> waitInUrl }}
|
|
941
|
+
*/
|
|
942
|
+
async waitInUrl(urlPart, sec = null) {
|
|
943
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
944
|
+
|
|
945
|
+
const clientFn = createClientFunction((urlPart) => {
|
|
946
|
+
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
|
|
947
|
+
return currUrl.indexOf(urlPart) > -1;
|
|
948
|
+
}, [urlPart]).with({ boundTestRun: this.t });
|
|
949
|
+
|
|
950
|
+
return waitForFunction(clientFn, waitTimeout).catch(async (err) => {
|
|
951
|
+
const currUrl = await this.grabCurrentUrl();
|
|
952
|
+
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* {{> waitUrlEquals }}
|
|
958
|
+
*/
|
|
959
|
+
async waitUrlEquals(urlPart, sec = null) {
|
|
960
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
961
|
+
|
|
962
|
+
const baseUrl = this.options.url;
|
|
963
|
+
if (urlPart.indexOf('http') < 0) {
|
|
964
|
+
urlPart = baseUrl + urlPart;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const clientFn = createClientFunction((urlPart) => {
|
|
968
|
+
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
|
|
969
|
+
return currUrl === urlPart;
|
|
970
|
+
}, [urlPart]).with({ boundTestRun: this.t });
|
|
971
|
+
|
|
972
|
+
return waitForFunction(clientFn, waitTimeout).catch(async (err) => {
|
|
973
|
+
const currUrl = await this.grabCurrentUrl();
|
|
974
|
+
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* {{> waitForFunction }}
|
|
980
|
+
*/
|
|
981
|
+
async waitForFunction(fn, argsOrSec = null, sec = null) {
|
|
982
|
+
let args = [];
|
|
983
|
+
if (argsOrSec) {
|
|
984
|
+
if (Array.isArray(argsOrSec)) {
|
|
985
|
+
args = argsOrSec;
|
|
986
|
+
} else if (typeof argsOrSec === 'number') {
|
|
987
|
+
sec = argsOrSec;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
991
|
+
|
|
992
|
+
const clientFn = createClientFunction((urlPart) => {
|
|
993
|
+
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)));
|
|
994
|
+
return currUrl.indexOf(urlPart) > -1;
|
|
995
|
+
}, args);
|
|
996
|
+
|
|
997
|
+
return waitForFunction(clientFn, waitTimeout);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* {{> waitNumberOfVisibleElements }}
|
|
1002
|
+
*/
|
|
1003
|
+
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
1004
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1005
|
+
|
|
1006
|
+
return this.t
|
|
1007
|
+
.expect(createSelector(locator).with({ boundTestRun: this.t }).filterVisible().count)
|
|
1008
|
+
.eql(num, `The number of elements (${locator}) is not ${num} after ${sec} sec`, { timeout: waitTimeout })
|
|
1009
|
+
.catch(mapError);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* {{> waitForElement }}
|
|
1014
|
+
*/
|
|
1015
|
+
async waitForElement(locator, sec) {
|
|
1016
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1017
|
+
|
|
1018
|
+
return this.t
|
|
1019
|
+
.expect(createSelector(locator).with({ boundTestRun: this.t }).exists)
|
|
1020
|
+
.ok({ timeout: waitTimeout });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* {{> waitToHide }}
|
|
1025
|
+
*/
|
|
1026
|
+
async waitToHide(locator, sec) {
|
|
1027
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1028
|
+
|
|
1029
|
+
return this.t
|
|
1030
|
+
.expect(createSelector(locator).filterHidden().with({ boundTestRun: this.t }).exists)
|
|
1031
|
+
.notOk({ timeout: waitTimeout });
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* {{> waitForInvisible }}
|
|
1036
|
+
*/
|
|
1037
|
+
async waitForInvisible(locator, sec) {
|
|
1038
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1039
|
+
|
|
1040
|
+
return this.t
|
|
1041
|
+
.expect(createSelector(locator).filterVisible().with({ boundTestRun: this.t }).exists)
|
|
1042
|
+
.ok({ timeout: waitTimeout });
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* {{> waitForText }}
|
|
1047
|
+
*
|
|
1048
|
+
*/
|
|
1049
|
+
async waitForText(text, sec = null, context = null) {
|
|
1050
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
1051
|
+
|
|
1052
|
+
let els;
|
|
1053
|
+
if (context) {
|
|
1054
|
+
els = (await findElements.call(this, this.context, context));
|
|
1055
|
+
await this.t
|
|
1056
|
+
.expect(els.exists)
|
|
1057
|
+
.ok(`Context element ${context} not found`, { timeout: waitTimeout });
|
|
1058
|
+
} else {
|
|
1059
|
+
els = (await findElements.call(this, this.context, '*'));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return this.t
|
|
1063
|
+
.expect(els.withText(text).filterVisible().exists)
|
|
1064
|
+
.ok(`No element with text "${text}" found in ${context || 'body'}`, { timeout: waitTimeout })
|
|
1065
|
+
.catch(mapError);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function waitForFunction(browserFn, waitTimeout) {
|
|
1070
|
+
const pause = () => new Promise((done => setTimeout(done, 50)));
|
|
1071
|
+
|
|
1072
|
+
const start = Date.now();
|
|
1073
|
+
// eslint-disable-next-line no-constant-condition
|
|
1074
|
+
while (true) {
|
|
1075
|
+
let result;
|
|
1076
|
+
try {
|
|
1077
|
+
result = await browserFn();
|
|
1078
|
+
// eslint-disable-next-line no-empty
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
throw new Error(`Error running function ${err.toString()}`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (result) return result;
|
|
1084
|
+
|
|
1085
|
+
const duration = (Date.now() - start);
|
|
1086
|
+
if (duration > waitTimeout) {
|
|
1087
|
+
throw new Error('waitForFunction timed out');
|
|
1088
|
+
}
|
|
1089
|
+
await pause(); // make polling
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const createSelector = (locator) => {
|
|
1094
|
+
locator = new Locator(locator, 'css');
|
|
1095
|
+
if (locator.isXPath()) return elementByXPath(locator.value);
|
|
1096
|
+
return Selector(locator.simplify());
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const elementByXPath = (xpath) => {
|
|
1100
|
+
assert(xpath, 'xpath is required');
|
|
1101
|
+
|
|
1102
|
+
return Selector(() => {
|
|
1103
|
+
const iterator = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
|
|
1104
|
+
const items = [];
|
|
1105
|
+
|
|
1106
|
+
let item = iterator.iterateNext();
|
|
1107
|
+
|
|
1108
|
+
while (item) {
|
|
1109
|
+
items.push(item);
|
|
1110
|
+
item = iterator.iterateNext();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return items;
|
|
1114
|
+
}, { dependencies: { xpath } });
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
const assertElementExists = async (res, locator, prefix, suffix) => {
|
|
1118
|
+
if (!res || !(await res.count) || !(await res.nth(0).tagName)) {
|
|
1119
|
+
throw new ElementNotFound(locator, prefix, suffix);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
async function findElements(matcher, locator) {
|
|
1124
|
+
if (locator && locator.react) throw new Error('react locators are not yet supported');
|
|
1125
|
+
|
|
1126
|
+
locator = new Locator(locator, 'css');
|
|
1127
|
+
|
|
1128
|
+
if (!locator.isXPath()) {
|
|
1129
|
+
return matcher
|
|
1130
|
+
? matcher.find(locator.simplify())
|
|
1131
|
+
: Selector(locator.simplify()).with({ timeout: 0, boundTestRun: this.t });
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (!matcher) return elementByXPath(locator.value).with({ timeout: 0, boundTestRun: this.t });
|
|
1135
|
+
|
|
1136
|
+
return matcher.find((node, idx, originNode) => {
|
|
1137
|
+
const found = document.evaluate(xpath, originNode, null, 5, null);
|
|
1138
|
+
let current = null;
|
|
1139
|
+
while (current = found.iterateNext()) {
|
|
1140
|
+
if (current === node) return true;
|
|
1141
|
+
}
|
|
1142
|
+
return false;
|
|
1143
|
+
}, { xpath: locator.value });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function proceedClick(locator, context = null) {
|
|
1147
|
+
let matcher;
|
|
1148
|
+
|
|
1149
|
+
if (context) {
|
|
1150
|
+
const els = await this._locate(context);
|
|
1151
|
+
await assertElementExists(els, context);
|
|
1152
|
+
matcher = await els.nth(0);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const els = await findClickable.call(this, matcher, locator);
|
|
1156
|
+
if (context) {
|
|
1157
|
+
await assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`);
|
|
1158
|
+
} else {
|
|
1159
|
+
await assertElementExists(els, locator, 'Clickable element');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const firstElement = await els.nth(0);
|
|
1163
|
+
|
|
1164
|
+
return this.t
|
|
1165
|
+
.click(firstElement)
|
|
1166
|
+
.catch(mapError);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function findClickable(matcher, locator) {
|
|
1170
|
+
if (locator && locator.react) throw new Error('react locators are not yet supported');
|
|
1171
|
+
|
|
1172
|
+
locator = new Locator(locator);
|
|
1173
|
+
if (!locator.isFuzzy()) return (await findElements.call(this, matcher, locator)).filterVisible();
|
|
1174
|
+
|
|
1175
|
+
let els;
|
|
1176
|
+
|
|
1177
|
+
// try to use native TestCafe locator
|
|
1178
|
+
els = matcher ? matcher.find('a,button') : createSelector('a,button');
|
|
1179
|
+
els = await els.withExactText(locator.value).with({ timeout: 0, boundTestRun: this.t });
|
|
1180
|
+
if (await els.count) return els;
|
|
1181
|
+
|
|
1182
|
+
const literal = xpathLocator.literal(locator.value);
|
|
1183
|
+
|
|
1184
|
+
els = (await findElements.call(this, matcher, Locator.clickable.narrow(literal))).filterVisible();
|
|
1185
|
+
if (await els.count) return els;
|
|
1186
|
+
|
|
1187
|
+
els = (await findElements.call(this, matcher, Locator.clickable.wide(literal))).filterVisible();
|
|
1188
|
+
if (await els.count) return els;
|
|
1189
|
+
|
|
1190
|
+
els = (await findElements.call(this, matcher, Locator.clickable.self(literal))).filterVisible();
|
|
1191
|
+
if (await els.count) return els;
|
|
1192
|
+
|
|
1193
|
+
return findElements.call(this, matcher, locator.value); // by css or xpath
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function proceedIsChecked(assertType, option) {
|
|
1197
|
+
const els = await findCheckable.call(this, option);
|
|
1198
|
+
assertElementExists(els, option, 'Checkable');
|
|
1199
|
+
|
|
1200
|
+
const selected = await els.checked;
|
|
1201
|
+
|
|
1202
|
+
return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function findCheckable(locator, context) {
|
|
1206
|
+
assert(locator, 'locator is required');
|
|
1207
|
+
assert(this.t, 'this.t is required');
|
|
1208
|
+
|
|
1209
|
+
let contextEl = await this.context;
|
|
1210
|
+
if (typeof context === 'string') {
|
|
1211
|
+
contextEl = (await findElements.call(this, contextEl, (new Locator(context, 'css')).simplify())).filterVisible();
|
|
1212
|
+
contextEl = await contextEl.nth(0);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const matchedLocator = new Locator(locator);
|
|
1216
|
+
if (!matchedLocator.isFuzzy()) {
|
|
1217
|
+
return (await findElements.call(this, contextEl, matchedLocator.simplify())).filterVisible();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const literal = xpathLocator.literal(locator);
|
|
1221
|
+
let els = (await findElements.call(this, contextEl, Locator.checkable.byText(literal))).filterVisible();
|
|
1222
|
+
if (await els.count) {
|
|
1223
|
+
return els;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
els = (await findElements.call(this, contextEl, Locator.checkable.byName(literal))).filterVisible();
|
|
1227
|
+
if (await els.count) {
|
|
1228
|
+
return els;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return (await findElements.call(this, contextEl, locator)).filterVisible();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async function findFields(locator) {
|
|
1235
|
+
const matchedLocator = new Locator(locator);
|
|
1236
|
+
if (!matchedLocator.isFuzzy()) {
|
|
1237
|
+
return this._locate(matchedLocator);
|
|
1238
|
+
}
|
|
1239
|
+
const literal = xpathLocator.literal(locator);
|
|
1240
|
+
|
|
1241
|
+
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) });
|
|
1242
|
+
if (await els.count) {
|
|
1243
|
+
return els;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
els = await this._locate({ xpath: Locator.field.labelContains(literal) });
|
|
1247
|
+
if (await els.count) {
|
|
1248
|
+
return els;
|
|
1249
|
+
}
|
|
1250
|
+
els = await this._locate({ xpath: Locator.field.byName(literal) });
|
|
1251
|
+
if (await els.count) {
|
|
1252
|
+
return els;
|
|
1253
|
+
}
|
|
1254
|
+
return this._locate({ css: locator });
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
module.exports = TestCafe;
|