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