codeceptjs
Version:
Modern Era Acceptance Testing Framework for NodeJS
1,746 lines (1,584 loc) • 75.8 kB
JavaScript
let webdriverio;
const assert = require('assert');
const path = require('path');
const requireg = require('requireg');
const Helper = require('../helper');
const stringIncludes = require('../assert/include').includes;
const { urlEquals, equals } = require('../assert/equal');
const { debug } = require('../output');
const empty = require('../assert/empty').empty;
const truth = require('../assert/truth').truth;
const {
xpathLocator,
fileExists,
decodeUrl,
chunkArray,
convertCssPropertiesToCamelCase,
screenshotOutputFolder,
getNormalizedKeyAttributeValue,
modifierKeys,
} = require('../utils');
const {
isColorProperty,
convertColorToRGBA,
} = require('../colorUtils');
const ElementNotFound = require('./errors/ElementNotFound');
const ConnectionRefused = require('./errors/ConnectionRefused');
const Locator = require('../locator');
const webRoot = 'body';
/**
* WebDriver helper which wraps [webdriverio](http://webdriver.io/) library to
* manipulate browser using Selenium WebDriver or PhantomJS.
*
* WebDriver requires [Selenium Server and ChromeDriver/GeckoDriver to be installed](http://codecept.io/quickstart/#prepare-selenium-server).
*
* ### Configuration
*
* This helper should be configured in codecept.json or codecept.conf.js
*
* * `url`: base url of website to be tested.
* * `browser`: browser in which to perform testing.
* * `host`: (optional, default: localhost) - WebDriver host to connect.
* * `port`: (optional, default: 4444) - WebDriver port to connect.
* * `protocol`: (optional, default: http) - protocol for WebDriver server.
* * `path`: (optional, default: /wd/hub) - path to WebDriver server,
* * `restart`: (optional, default: true) - restart browser between tests.
* * `smartWait`: (optional) **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.
* * `disableScreenshots`: (optional, default: false) - don't save screenshots on failure.
* * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
* * `uniqueScreenshotNames`: (optional, default: false) - option to prevent screenshot override if you have scenarios with the same name in different suites.
* * `keepBrowserState`: (optional, default: false) - keep browser state between tests when `restart` is set to false.
* * `keepCookies`: (optional, default: false) - keep cookies between tests when `restart` set to false.
* * `windowSize`: (optional) default window size. Set to `maximize` or a dimension in the format `640x480`.
* * `waitForTimeout`: (optional, default: 1000) sets default wait time in *ms* for all `wait*` functions.
* * `desiredCapabilities`: Selenium's [desired
* capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities).
* * `manualStart`: (optional, default: false) - do not start browser before a test, start it manually inside a helper
* with `this.helpers["WebDriver"]._startBrowser()`.
* * `timeouts`: [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash.
*
* Example:
*
* ```js
* {
* helpers: {
* WebDriver : {
* smartWait: 5000,
* browser: "chrome",
* restart: false,
* windowSize: "maximize",
* timeouts: {
* "script": 60000,
* "page load": 10000
* }
* }
* }
* }
* ```
*
* Additional configuration params can be used from [webdriverio
* website](http://webdriver.io/guide/getstarted/configuration.html).
*
* ### Headless Chrome
*
* ```js
* {
* helpers: {
* WebDriver : {
* url: "http://localhost",
* browser: "chrome",
* desiredCapabilities: {
* chromeOptions: {
* args: [ "--headless", "--disable-gpu", "--no-sandbox" ]
* }
* }
* }
* }
* }
* ```
*
* ### Internet Explorer
*
* Additional configuration params can be used from [IE options](https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/IE/Options.html)
*
* ```js
* {
* helpers: {
* WebDriver : {
* url: "http://localhost",
* browser: "internet explorer",
* desiredCapabilities: {
* ieOptions: {
* "ie.browserCommandLineSwitches": "-private",
* "ie.usePerProcessProxy": true,
* "ie.ensureCleanSession": true,
* }
* }
* }
* }
* }
* ```
*
* ### Selenoid Options
*
* [Selenoid](https://aerokube.com/selenoid/latest/) is a modern way to run Selenium inside Docker containers.
* Selenoid is easy to set up and provides more features than original Selenium Server. Use `selenoidOptions` to set Selenoid capabilities
*
* ```js
* {
* helpers: {
* WebDriver : {
* url: "http://localhost",
* browser: "chrome",
* desiredCapabilities: {
* selenoidOptions: {
* enableVNC: true,
* }
* }
* }
* }
* }
* ```
*
* ### Connect Through proxy
*
* CodeceptJS also provides flexible options when you want to execute tests to Selenium servers through proxy. You will
* need to update the `helpers.WebDriver.capabilities.proxy` key.
*
* ```js
* {
* helpers: {
* WebDriver: {
* capabilities: {
* proxy: {
* "proxyType": "manual|pac",
* "proxyAutoconfigUrl": "URL TO PAC FILE",
* "httpProxy": "PROXY SERVER",
* "sslProxy": "PROXY SERVER",
* "ftpProxy": "PROXY SERVER",
* "socksProxy": "PROXY SERVER",
* "socksUsername": "USERNAME",
* "socksPassword": "PASSWORD",
* "noProxy": "BYPASS ADDRESSES"
* }
* }
* }
* }
* }
* ```
* For example,
*
* ```js
* {
* helpers: {
* WebDriver: {
* capabilities: {
* proxy: {
* "proxyType": "manual",
* "httpProxy": "http://corporate.proxy:8080",
* "socksUsername": "codeceptjs",
* "socksPassword": "secret",
* "noProxy": "127.0.0.1,localhost"
* }
* }
* }
* }
* }
* ```
*
* Please refer to [Selenium - Proxy Object](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) for more
* information.
*
* ### Cloud Providers
*
* WebDriver makes it possible to execute tests against services like `Sauce Labs` `BrowserStack` `TestingBot`
* Check out their documentation on [available parameters](http://webdriver.io/guide/usage/cloudservices.html)
*
* Connecting to `BrowserStack` and `Sauce Labs` is simple. All you need to do
* is set the `user` and `key` parameters. WebDriver automatically know which
* service provider to connect to.
*
* ```js
* {
* helpers:{
* WebDriver: {
* url: "YOUR_DESIRED_HOST",
* user: "YOUR_BROWSERSTACK_USER",
* key: "YOUR_BROWSERSTACK_KEY",
* capabilities: {
* "browserName": "chrome",
*
* // only set this if you're using BrowserStackLocal to test a local domain
* // "browserstack.local": true,
*
* // set this option to tell browserstack to provide addition debugging info
* // "browserstack.debug": true,
* }
* }
* }
* }
* ```
*
* #### SauceLabs
*
* SauceLabs can be configured via wdio service, which should be installed additionally:
*
* ```
* npm i @wdio/sauce-service --save
* ```
*
* It is important to make sure it is compatible with current webdriverio version.
*
* Enable `wdio` plugin in plugins list and add `sauce` service:
*
* ```js
* plugins: {
* wdio: {
* enabled: true,
* services: ['sauce'],
* user: ... ,// saucelabs username
* key: ... // saucelabs api key
* // additional config, from sauce service
* }
* }
* ```
*
* See [complete reference on webdriver.io](https://webdriver.io/docs/sauce-service.html).
*
* > Alternatively, use [codeceptjs-saucehelper](https://github.com/puneet0191/codeceptjs-saucehelper/) for better reporting.
*
* #### BrowserStack
*
* BrowserStack can be configured via wdio service, which should be installed additionally:
*
* ```
* npm i @wdio/browserstack-service --save
* ```
*
* It is important to make sure it is compatible with current webdriverio version.
*
* Enable `wdio` plugin in plugins list and add `browserstack` service:
*
* ```js
* plugins: {
* wdio: {
* enabled: true,
* services: ['browserstack'],
* user: ... ,// browserstack username
* key: ... // browserstack api key
* // additional config, from browserstack service
* }
* }
* ```
*
* See [complete reference on webdriver.io](https://webdriver.io/docs/browserstack-service.html).
*
* > Alternatively, use [codeceptjs-bshelper](https://github.com/PeterNgTr/codeceptjs-bshelper) for better reporting.
*
* #### TestingBot
*
* > **Recommended**: use official [TestingBot Helper](https://github.com/testingbot/codeceptjs-tbhelper).
*
* Alternatively, TestingBot can be configured via wdio service, which should be installed additionally:
*
* ```
* npm i @wdio/testingbot-service --save
* ```
*
* It is important to make sure it is compatible with current webdriverio version.
*
* Enable `wdio` plugin in plugins list and add `testingbot` service:
*
* ```js
* plugins: {
* wdio: {
* enabled: true,
* services: ['testingbot'],
* user: ... ,// testingbot key
* key: ... // testingbot secret
* // additional config, from testingbot service
* }
* }
* ```
*
* See [complete reference on webdriver.io](https://webdriver.io/docs/testingbot-service.html).
*
* #### Applitools
*
* Visual testing via Applitools service
*
* > Use [CodeceptJS Applitools Helper](https://github.com/PeterNgTr/codeceptjs-applitoolshelper) with Applitools wdio service.
*
*
* ### Multiremote Capabilities
*
* This is a work in progress but you can control two browsers at a time right out of the box.
* Individual control is something that is planned for a later version.
*
* Here is the [webdriverio docs](http://webdriver.io/guide/usage/multiremote.html) on the subject
*
* ```js
* {
* helpers: {
* WebDriver: {
* "multiremote": {
* "MyChrome": {
* "desiredCapabilities": {
* "browserName": "chrome"
* }
* },
* "MyFirefox": {
* "desiredCapabilities": {
* "browserName": "firefox"
* }
* }
* }
* }
* }
* }
* ```
*
* ## Access From Helpers
*
* Receive a WebDriver client from a custom helper by accessing `browser` property:
*
* ```js
* const { WebDriver } = this.helpers;
* const browser = WebDriver.browser
* ```
*
* ## Methods
*/
class WebDriver extends Helper {
constructor(config) {
super(config);
webdriverio = requireg('webdriverio');
if (webdriverio.VERSION && webdriverio.VERSION.indexOf('4') === 0) {
throw new Error(`This helper is compatible with "webdriverio@5". Current version: ${webdriverio.VERSION}. Please upgrade webdriverio to v5+ or use WebDriverIO helper instead`);
}
// set defaults
this.root = webRoot;
this.isWeb = true;
this.isRunning = false;
this._setConfig(config);
Locator.addFilter((locator, result) => {
if (typeof locator === 'string' && locator.indexOf('~') === 0) {
// accessibility locator
if (this.isWeb) {
result.value = `[aria-label="${locator.slice(1)}"]`;
result.type = 'css';
result.output = `aria-label=${locator.slice(1)}`;
}
}
});
}
_validateConfig(config) {
const defaults = {
logLevel: 'silent',
// codeceptjs
remoteFileUpload: true,
smartWait: 0,
waitForTimeout: 1000, // ms
capabilities: {},
restart: true,
uniqueScreenshotNames: false,
disableScreenshots: false,
fullPageScreenshots: false,
manualStart: false,
keepCookies: false,
keepBrowserState: false,
deprecationWarnings: false,
timeouts: {
script: 1000, // ms
},
};
// override defaults with config
config = Object.assign(defaults, config);
if (typeof config.host !== 'undefined') config.hostname = config.host; // webdriverio spec
config.baseUrl = config.url || config.baseUrl;
if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
config.capabilities = config.desiredCapabilities;
}
config.capabilities.browserName = config.browser || config.capabilities.browserName;
if (config.capabilities.chromeOptions) {
config.capabilities['goog:chromeOptions'] = config.capabilities.chromeOptions;
delete config.capabilities.chromeOptions;
}
if (config.capabilities.firefoxOptions) {
config.capabilities['moz:firefoxOptions'] = config.capabilities.firefoxOptions;
delete config.capabilities.firefoxOptions;
}
if (config.capabilities.ieOptions) {
config.capabilities['se:ieOptions'] = config.capabilities.ieOptions;
delete config.capabilities.ieOptions;
}
if (config.capabilities.selenoidOptions) {
config.capabilities['selenoid:options'] = config.capabilities.selenoidOptions;
delete config.capabilities.selenoidOptions;
}
config.waitForTimeout /= 1000; // convert to seconds
if (!config.capabilities.platformName && (!config.url || !config.browser)) {
throw new Error(`
WebDriver requires at url and browser to be set.
Check your codeceptjs config file to ensure these are set properly
{
"helpers": {
"WebDriver": {
"url": "YOUR_HOST"
"browser": "YOUR_PREFERRED_TESTING_BROWSER"
}
}
}
`);
}
return config;
}
static _checkRequirements() {
try {
requireg('webdriverio');
} catch (e) {
return ['webdriverio@^5.2.2'];
}
}
static _config() {
return [{
name: 'url',
message: 'Base url of site to be tested',
default: 'http://localhost',
}, {
name: 'browser',
message: 'Browser in which testing will be performed',
default: 'chrome',
}];
}
_beforeSuite() {
if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
this.debugSection('Session', 'Starting singleton browser session');
return this._startBrowser();
}
}
async _startBrowser() {
try {
if (this.options.multiremote) {
this.browser = await webdriverio.multiremote(this.options.multiremote);
} else {
this.browser = await webdriverio.remote(this.options);
}
} catch (err) {
if (err.toString().indexOf('ECONNREFUSED')) {
throw new ConnectionRefused(err);
}
throw err;
}
this.isRunning = true;
if (this.options.timeouts && this.isWeb) {
await this.defineTimeout(this.options.timeouts);
}
await this._resizeWindowIfNeeded(this.browser, this.options.windowSize);
this.$$ = this.browser.$$.bind(this.browser);
return this.browser;
}
async _stopBrowser() {
if (this.browser && this.isRunning) await this.browser.deleteSession();
}
async _before() {
this.context = this.root;
if (this.options.restart && !this.options.manualStart) return this._startBrowser();
if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
this.$$ = this.browser.$$.bind(this.browser);
return this.browser;
}
async _after() {
if (!this.isRunning) return;
if (this.options.restart) {
this.isRunning = false;
return this.browser.deleteSession();
}
if (this.browser.isInsideFrame) await this.browser.switchToFrame(null);
if (this.options.keepBrowserState) return;
if (!this.options.keepCookies && this.options.capabilities.browserName) {
this.debugSection('Session', 'cleaning cookies and localStorage');
await this.browser.deleteCookies();
}
await this.browser.execute('localStorage.clear();').catch((err) => {
if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err;
});
await this.closeOtherTabs();
return this.browser;
}
_afterSuite() {
}
_finishTest() {
if (!this.options.restart && this.isRunning) return this._stopBrowser();
}
_session() {
const defaultSession = this.browser;
return {
start: async (opts) => {
// opts.disableScreenshots = true; // screenshots cant be saved as session will be already closed
opts = this._validateConfig(Object.assign(this.options, opts));
this.debugSection('New Browser', JSON.stringify(opts));
const browser = await webdriverio.remote(opts);
if (opts.timeouts && this.isWeb) {
await this._defineBrowserTimeout(browser, opts.timeouts);
}
await this._resizeWindowIfNeeded(browser, opts.windowSize);
return browser;
},
stop: async (browser) => {
return browser.deleteSession();
},
loadVars: async (browser) => {
if (this.context !== this.root) throw new Error('Can\'t start session inside within block');
this.browser = browser;
this.$$ = this.browser.$$.bind(this.browser);
},
restoreVars: async () => {
this.browser = defaultSession;
this.$$ = this.browser.$$.bind(this.browser);
},
};
}
async _failed(test) {
if (this.context !== this.root) await this._withinEnd();
}
async _withinBegin(locator) {
const frame = isFrameLocator(locator);
if (frame) {
this.browser.isInsideFrame = true;
if (Array.isArray(frame)) {
// this.switchTo(null);
await forEachAsync(frame, async f => this.switchTo(f));
return;
}
await this.switchTo(frame);
return;
}
this.context = locator;
let res = await this.browser.$$(withStrictLocator(locator));
assertElementExists(res, locator);
res = usingFirstElement(res);
this.context = res.selector;
this.$$ = res.$$.bind(res);
}
async _withinEnd() {
if (this.browser.isInsideFrame) {
this.browser.isInsideFrame = false;
return this.switchTo(null);
}
this.context = this.root;
this.$$ = this.browser.$$.bind(this.browser);
}
/**
* Get elements by different locator types, including strict locator.
* Should be used in custom helpers:
*
* ```js
* this.helpers['WebDriver']._locate({name: 'password'}).then //...
* ```
*
*
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
*/
async _locate(locator, smartWait = false) {
if (require('../store').debugMode) smartWait = false;
// special locator type for React
if (locator.react) {
const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined);
this.debugSection('Elements', `Found ${els.length} react components`);
return els;
}
if (!this.options.smartWait || !smartWait) {
const els = await this.$$(withStrictLocator(locator));
return els;
}
this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);
await this.defineTimeout({ implicit: this.options.smartWait });
const els = await this.$$(withStrictLocator(locator));
await this.defineTimeout({ implicit: 0 });
return els;
}
/**
* Find a checkbox by providing human readable text:
*
* ```js
* this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ...
* ```
*
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
*/
async _locateCheckable(locator) {
return findCheckable.call(this, locator, this.$$.bind(this)).then(res => res);
}
/**
* Find a clickable element by providing human readable text:
*
* ```js
* this.helpers['WebDriver']._locateClickable('Next page').then // ...
* ```
*
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
*/
async _locateClickable(locator) {
return findClickable.call(this, locator, this.$$.bind(this)).then(res => res);
}
/**
* Find field elements by providing human readable text:
*
* ```js
* this.helpers['WebDriver']._locateFields('Your email').then // ...
* ```
*
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
*/
async _locateFields(locator) {
return findFields.call(this, locator).then(res => res);
}
/**
* Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
*
* Timeouts are expected to be passed as object:
*
* ```js
* I.defineTimeout({ script: 5000 });
* I.defineTimeout({ implicit: 10000, pageLoad: 10000, script: 5000 });
* ```
*
* @param {WebdriverIO.Timeouts} timeouts WebDriver timeouts object.
*/
defineTimeout(timeouts) {
return this._defineBrowserTimeout(this.browser, timeouts);
}
_defineBrowserTimeout(browser, timeouts) {
return browser.setTimeout(timeouts);
}
/**
* {{> amOnPage }}
*
*/
amOnPage(url) {
return this.browser.url(url);
}
/**
* {{> click }}
*
* {{ react }}
*/
async click(locator, context = null) {
const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
const locateFn = prepareLocateFn.call(this, context);
const res = await findClickable.call(this, locator, locateFn);
if (context) {
assertElementExists(res, locator, 'Clickable element', `was not found inside element ${new Locator(context)}`);
} else {
assertElementExists(res, locator, 'Clickable element');
}
const elem = usingFirstElement(res);
return this.browser[clickMethod](getElementId(elem));
}
/**
* {{> doubleClick }}
*
* {{ react }}
*/
async doubleClick(locator, context = null) {
const locateFn = prepareLocateFn.call(this, context);
const res = await findClickable.call(this, locator, locateFn);
if (context) {
assertElementExists(res, locator, 'Clickable element', `was not found inside element ${new Locator(context)}`);
} else {
assertElementExists(res, locator, 'Clickable element');
}
const elem = usingFirstElement(res);
return elem.doubleClick();
}
/**
* {{> rightClick }}
*
* {{ react }}
*/
async rightClick(locator, context) {
const locateFn = prepareLocateFn.call(this, context);
const res = await findClickable.call(this, locator, locateFn);
if (context) {
assertElementExists(res, locator, 'Clickable element', `was not found inside element ${new Locator(context)}`);
} else {
assertElementExists(res, locator, 'Clickable element');
}
const el = usingFirstElement(res);
await el.moveTo();
if (this.browser.isW3C) {
// W3C version
return this.browser.performActions([
{ type: 'pointerDown', button: 2 },
]);
}
// JSON Wire version
await this.browser.buttonDown(2);
}
/**
* {{> fillField }}
* {{ react }}
*
*/
async fillField(field, value) {
const res = await findFields.call(this, field);
assertElementExists(res, field, 'Field');
const elem = usingFirstElement(res);
return elem.setValue(value.toString());
}
/**
* {{> appendField }}
* {{ react }}
*/
async appendField(field, value) {
const res = await findFields.call(this, field);
assertElementExists(res, field, 'Field');
const elem = usingFirstElement(res);
return elem.addValue(value);
}
/**
* {{> clearField }}
*
*/
async clearField(field) {
const res = await findFields.call(this, field);
assertElementExists(res, field, 'Field');
const elem = usingFirstElement(res);
return elem.clearValue(getElementId(elem));
}
/**
* {{> selectOption }}
*/
async selectOption(select, option) {
const res = await findFields.call(this, select);
assertElementExists(res, select, 'Selectable field');
const elem = usingFirstElement(res);
if (!Array.isArray(option)) {
option = [option];
}
// select options by visible text
let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))));
const clickOptionFn = async (el) => {
if (el[0]) el = el[0];
const elementId = getElementId(el);
if (elementId) return this.browser.elementClick(elementId);
};
if (Array.isArray(els) && els.length) {
return forEachAsync(els, clickOptionFn);
}
// select options by value
els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byValue(xpathLocator.literal(opt))));
if (els.length === 0) {
throw new ElementNotFound(select, `Option "${option}" in`, 'was not found neither by a visible text nor by a value');
}
return forEachAsync(els, clickOptionFn);
}
/**
* {{> attachFile }}
* Appium: not tested
*/
async attachFile(locator, pathToFile) {
let file = path.join(global.codecept_dir, pathToFile);
if (!fileExists(file)) {
throw new Error(`File at ${file} can not be found on local system`);
}
const res = await findFields.call(this, locator);
this.debug(`Uploading ${file}`);
assertElementExists(res, locator, 'File field');
const el = usingFirstElement(res);
// Remote Upload (when running Selenium Server)
if (this.options.remoteFileUpload) {
try {
this.debugSection('File', 'Uploading file to remote server');
file = await this.browser.uploadFile(file);
} catch (err) {
throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`);
}
}
return el.addValue(file);
}
/**
* {{> checkOption }}
* Appium: not tested
*/
async checkOption(field, context = null) {
const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
const locateFn = prepareLocateFn.call(this, context);
const res = await findCheckable.call(this, field, locateFn);
assertElementExists(res, field, 'Checkable');
const elem = usingFirstElement(res);
const elementId = getElementId(elem);
const isSelected = await this.browser.isElementSelected(elementId);
if (isSelected) return Promise.resolve(true);
return this.browser[clickMethod](elementId);
}
/**
* {{> uncheckOption }}
* Appium: not tested
*/
async uncheckOption(field, context = null) {
const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementClick';
const locateFn = prepareLocateFn.call(this, context);
const res = await findCheckable.call(this, field, locateFn);
assertElementExists(res, field, 'Checkable');
const elem = usingFirstElement(res);
const elementId = getElementId(elem);
const isSelected = await this.browser.isElementSelected(elementId);
if (!isSelected) return Promise.resolve(true);
return this.browser[clickMethod](elementId);
}
/**
* {{> grabTextFrom }}
*
*/
async grabTextFrom(locator) {
const res = await this._locate(locator, true);
assertElementExists(res, locator);
let val;
if (res.length > 1) {
val = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)));
} else {
val = await this.browser.getElementText(getElementId(res[0]));
}
this.debugSection('Grab', val);
return val;
}
/**
* {{> grabHTMLFrom }}
*
*/
async grabHTMLFrom(locator) {
const elems = await this._locate(locator, true);
assertElementExists(elems, locator);
const values = await Promise.all(elems.map(elem => elem.getHTML(false)));
this.debugSection('Grab', values);
if (Array.isArray(values) && values.length === 1) {
return values[0];
}
return values;
}
/**
* {{> grabValueFrom }}
*
*/
async grabValueFrom(locator) {
const res = await this._locate(locator, true);
assertElementExists(res, locator);
return forEachAsync(res, async el => el.getValue());
}
/**
* {{> grabCssPropertyFrom }}
*/
async grabCssPropertyFrom(locator, cssProperty) {
const res = await this._locate(locator, true);
assertElementExists(res, locator);
return forEachAsync(res, async el => this.browser.getElementCSSValue(getElementId(el), cssProperty));
}
/**
* {{> grabAttributeFrom }}
* Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
*/
async grabAttributeFrom(locator, attr) {
const res = await this._locate(locator, true);
assertElementExists(res, locator);
return forEachAsync(res, async el => el.getAttribute(attr));
}
/**
* {{> seeInTitle }}
*
*/
async seeInTitle(text) {
const title = await this.browser.getTitle();
return stringIncludes('web page title').assert(text, title);
}
/**
* Checks that title is equal to provided one.
*
* ```js
* I.seeTitleEquals('Test title.');
* ```
*
* @param {string} text value to check.
*/
async seeTitleEquals(text) {
const title = await this.browser.getTitle();
return assert.equal(title, text, `expected web page title to be ${text}, but found ${title}`);
}
/**
* {{> dontSeeInTitle }}
*
*/
async dontSeeInTitle(text) {
const title = await this.browser.getTitle();
return stringIncludes('web page title').negate(text, title);
}
/**
* {{> grabTitle }}
*
*/
async grabTitle() {
const title = await this.browser.getTitle();
this.debugSection('Title', title);
return title;
}
/**
* {{> see }}
*
* {{ react }}
*/
async see(text, context = null) {
return proceedSee.call(this, 'assert', text, context);
}
/**
* Checks that text is equal to provided one.
*
* ```js
* I.seeTextEquals('text', 'h1');
* ```
*
* @param {string} text element value to check.
* @param {CodeceptJS.LocatorOrString?} [context] (optional) element located by CSS|XPath|strict locator.
*/
async seeTextEquals(text, context = null) {
return proceedSee.call(this, 'assert', text, context, true);
}
/**
* {{> dontSee }}
*
* {{ react }}
*/
async dontSee(text, context = null) {
return proceedSee.call(this, 'negate', text, context);
}
/**
* {{> seeInField }}
*
*/
async seeInField(field, value) {
return proceedSeeField.call(this, 'assert', field, value);
}
/**
* {{> dontSeeInField }}
*
*/
async dontSeeInField(field, value) {
return proceedSeeField.call(this, 'negate', field, value);
}
/**
* {{> seeCheckboxIsChecked }}
* Appium: not tested
*/
async seeCheckboxIsChecked(field) {
return proceedSeeCheckbox.call(this, 'assert', field);
}
/**
* {{> dontSeeCheckboxIsChecked }}
* Appium: not tested
*/
async dontSeeCheckboxIsChecked(field) {
return proceedSeeCheckbox.call(this, 'negate', field);
}
/**
* {{> seeElement }}
* {{ react }}
*
*/
async seeElement(locator) {
const res = await this._locate(locator, true);
assertElementExists(res, locator);
const selected = await forEachAsync(res, async el => el.isDisplayed());
return truth(`elements of ${locator}`, 'to be seen').assert(selected);
}
/**
* {{> dontSeeElement }}
* {{ react }}
*/
async dontSeeElement(locator) {
const res = await this._locate(locator, false);
if (!res || res.length === 0) {
return truth(`elements of ${locator}`, 'to be seen').negate(false);
}
const selected = await forEachAsync(res, async el => el.isDisplayed());
return truth(`elements of ${locator}`, 'to be seen').negate(selected);
}
/**
* {{> seeElementInDOM }}
*
*/
async seeElementInDOM(locator) {
const res = await this.$$(withStrictLocator(locator));
return empty('elements').negate(res);
}
/**
* {{> dontSeeElementInDOM }}
*
*/
async dontSeeElementInDOM(locator) {
const res = await this.$$(withStrictLocator(locator));
return empty('elements').assert(res);
}
/**
* {{> seeInSource }}
*
*/
async seeInSource(text) {
const source = await this.browser.getPageSource();
return stringIncludes('HTML source of a page').assert(text, source);
}
/**
* {{> grabSource }}
*
*/
async grabSource() {
return this.browser.getPageSource();
}
/**
* Get JS log from browser. Log buffer is reset after each request.
*
* ```js
* let logs = await I.grabBrowserLogs();
* console.log(JSON.stringify(logs))
* ```
* @returns {Promise<string|undefined>}
*/
async grabBrowserLogs() {
if (this.browser.isW3C) {
this.debug('Logs not awailable in W3C specification');
return;
}
return this.browser.getLogs('browser');
}
/**
* {{> grabCurrentUrl }}
*/
async grabCurrentUrl() {
const res = await this.browser.getUrl();
this.debugSection('Url', res);
return res;
}
/**
* {{> dontSeeInSource }}
*/
async dontSeeInSource(text) {
const source = await this.browser.getPageSource();
return stringIncludes('HTML source of a page').negate(text, source);
}
/**
* {{> seeNumberOfElements }}
* {{ react }}
*/
async seeNumberOfElements(locator, num) {
const res = await this._locate(locator);
return assert.equal(res.length, num, `expected number of elements (${locator}) is ${num}, but found ${res.length}`);
}
/**
* {{> seeNumberOfVisibleElements }}
* {{ react }}
*/
async seeNumberOfVisibleElements(locator, num) {
const res = await this.grabNumberOfVisibleElements(locator);
return assert.equal(res, num, `expected number of visible elements (${locator}) is ${num}, but found ${res}`);
}
/**
* {{> seeCssPropertiesOnElements }}
*/
async seeCssPropertiesOnElements(locator, cssProperties) {
const res = await this._locate(locator);
assertElementExists(res, locator);
const elemAmount = res.length;
let props = await forEachAsync(res, async (el) => {
return forEachAsync(Object.keys(cssProperties), async (prop) => {
const propValue = await this.browser.getElementCSSValue(getElementId(el), prop);
if (isColorProperty(prop) && propValue && propValue.value) {
return convertColorToRGBA(propValue.value);
}
return propValue;
});
});
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
if (!Array.isArray(props)) props = [props];
let chunked = chunkArray(props, values.length);
chunked = chunked.filter((val) => {
for (let i = 0; i < val.length; ++i) {
if (val[i] !== values[i]) return false;
}
return true;
});
return assert.ok(
chunked.length === elemAmount,
`expected all elements (${locator}) to have CSS property ${JSON.stringify(cssProperties)}`,
);
}
/**
* {{> seeAttributesOnElements }}
*/
async seeAttributesOnElements(locator, attributes) {
const res = await this._locate(locator);
assertElementExists(res, locator);
const elemAmount = res.length;
let attrs = await forEachAsync(res, async (el) => {
return forEachAsync(Object.keys(attributes), async attr => el.getAttribute(attr));
});
const values = Object.keys(attributes).map(key => attributes[key]);
if (!Array.isArray(attrs)) attrs = [attrs];
let chunked = chunkArray(attrs, values.length);
chunked = chunked.filter((val) => {
for (let i = 0; i < val.length; ++i) {
if (val[i] !== values[i]) return false;
}
return true;
});
return assert.ok(
chunked.length === elemAmount,
`expected all elements (${locator}) to have attributes ${JSON.stringify(attributes)}`,
);
}
/**
* {{> grabNumberOfVisibleElements }}
*/
async grabNumberOfVisibleElements(locator) {
const res = await this._locate(locator);
let selected = await forEachAsync(res, async el => el.isDisplayed());
if (!Array.isArray(selected)) selected = [selected];
selected = selected.filter(val => val === true);
return selected.length;
}
/**
* {{> seeInCurrentUrl }}
*
*/
async seeInCurrentUrl(url) {
const res = await this.browser.getUrl();
return stringIncludes('url').assert(url, decodeUrl(res));
}
/**
* {{> dontSeeInCurrentUrl }}
*
*/
async dontSeeInCurrentUrl(url) {
const res = await this.browser.getUrl();
return stringIncludes('url').negate(url, decodeUrl(res));
}
/**
* {{> seeCurrentUrlEquals }}
*
*/
async seeCurrentUrlEquals(url) {
const res = await this.browser.getUrl();
return urlEquals(this.options.url).assert(url, decodeUrl(res));
}
/**
* {{> dontSeeCurrentUrlEquals }}
*
*/
async dontSeeCurrentUrlEquals(url) {
const res = await this.browser.getUrl();
return urlEquals(this.options.url).negate(url, decodeUrl(res));
}
/**
* {{> executeScript }}
*
*
* Wraps [execute](http://webdriver.io/api/protocol/execute.html) command.
*/
executeScript(fn) {
return this.browser.execute.apply(this.browser, arguments);
}
/**
* {{> executeAsyncScript }}
*
*/
executeAsyncScript(fn) {
return this.browser.executeAsync.apply(this.browser, arguments);
}
/**
* {{> scrollTo }}
*
*/
async scrollTo(locator, offsetX = 0, offsetY = 0) {
if (typeof locator === 'number' && typeof offsetX === 'number') {
offsetY = offsetX;
offsetX = locator;
locator = null;
}
if (locator) {
const res = await this._locate(withStrictLocator(locator), true);
assertElementExists(res);
const elem = usingFirstElement(res);
const elementId = getElementId(elem);
if (this.browser.isMobile) return this.browser.touchScroll(offsetX, offsetY, elementId);
const location = await elem.getLocation();
assertElementExists(location, 'Failed to receive', 'location');
/* eslint-disable prefer-arrow-callback */
return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, location.x + offsetX, location.y + offsetY);
/* eslint-enable */
}
if (this.browser.isMobile) return this.browser.touchScroll(locator, offsetX, offsetY);
/* eslint-disable prefer-arrow-callback, comma-dangle */
return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY);
/* eslint-enable */
}
/**
* {{> moveCursorTo }}
*
*/
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
const res = await this._locate(withStrictLocator(locator), true);
assertElementExists(res, locator);
const elem = usingFirstElement(res);
return elem.moveTo(offsetX, offsetY);
}
/**
* {{> saveScreenshot }}
*
*/
async saveScreenshot(fileName, fullPage = false) {
const outputFile = screenshotOutputFolder(fileName);
if (!fullPage) {
this.debug(`Screenshot has been saved to ${outputFile}`);
return this.browser.saveScreenshot(outputFile);
}
/* eslint-disable prefer-arrow-callback, comma-dangle, prefer-const */
const originalWindowSize = await this.browser.getWindowSize();
let { width, height } = await this.browser.execute(function () {
return {
height: document.body.scrollHeight,
width: document.body.scrollWidth
};
}).then(res => res);
if (height < 100) height = 500; // errors for very small height
/* eslint-enable */
await this.browser.setWindowSize(width, height);
this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`);
const buffer = await this.browser.saveScreenshot(outputFile);
await this.browser.setWindowSize(originalWindowSize.width, originalWindowSize.height);
return buffer;
}
/**
* {{> setCookie }}
*
*
* Uses Selenium's JSON [cookie
* format](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object).
*/
async setCookie(cookie) {
return this.browser.setCookies(cookie);
}
/**
* {{> clearCookie }}
*
*/
async clearCookie(cookie) {
return this.browser.deleteCookies(cookie);
}
/**
* {{> seeCookie }}
*
*/
async seeCookie(name) {
const cookie = await this.browser.getCookies([name]);
return truth(`cookie ${name}`, 'to be set').assert(cookie);
}
/**
* {{> dontSeeCookie }}
*
*/
async dontSeeCookie(name) {
const cookie = await this.browser.getCookies([name]);
return truth(`cookie ${name}`, 'to be set').negate(cookie);
}
/**
* {{> grabCookie }}
*
*/
async grabCookie(name) {
if (!name) return this.browser.getCookies();
const cookie = await this.browser.getCookies([name]);
this.debugSection('Cookie', JSON.stringify(cookie));
return cookie[0];
}
/**
* Accepts the active JavaScript native popup window, as created by window.alert|window.confirm|window.prompt.
* Don't confuse popups with modal windows, as created by [various
* libraries](http://jster.net/category/windows-modals-popups).
*/
async acceptPopup() {
return this.browser.getAlertText().then((res) => {
if (res !== null) {
return this.browser.acceptAlert();
}
});
}
/**
* Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
*
*/
async cancelPopup() {
return this.browser.getAlertText().then((res) => {
if (res !== null) {
return this.browser.dismissAlert();
}
});
}
/**
* Checks that the active JavaScript popup, as created by `window.alert|window.confirm|window.prompt`, contains the
* given string.
*
* @param {string} text value to check.
*/
async seeInPopup(text) {
return this.browser.getAlertText().then((res) => {
if (res === null) {
throw new Error('Popup is not opened');
}
stringIncludes('text in popup').assert(text, res);
});
}
/**
* Grab the text within the popup. If no popup is visible then it will return null.
*
* ```js
* await I.grabPopupText();
* ```
*/
async grabPopupText() {
try {
return await this.browser.getAlertText();
} catch (err) {
this.debugSection('Popup', 'Error getting text from popup');
}
}
/**
* {{> pressKeyDown }}
*/
async pressKeyDown(key) {
key = getNormalizedKey.call(this, key);
if (!this.browser.isW3C) {
return this.browser.sendKeys([key]);
}
return this.browser.performActions([{
type: 'key',
id: 'keyboard',
actions: [{
type: 'keyDown',
value: key,
}],
}]);
}
/**
* {{> pressKeyUp }}
*/
async pressKeyUp(key) {
key = getNormalizedKey.call(this, key);
if (!this.browser.isW3C) {
return this.browser.sendKeys([key]);
}
return this.browser.performActions([{
type: 'key',
id: 'keyboard',
actions: [{
type: 'keyUp',
value: key,
}],
}]);
}
/**
* {{> pressKeyWithKeyNormalization }}
*
* _Note:_ In case a text field or textarea is focused be aware that some browsers do not respect active modifier when combining modifier keys with other keys.
*/
async pressKey(key) {
const modifiers = [];
if (Array.isArray(key)) {
for (let k of key) {
k = getNormalizedKey.call(this, k);
if (isModifierKey(k)) {
modifiers.push(k);
} else {
key = k;
break;
}
}
} else {
key = getNormalizedKey.call(this, key);
}
for (const modifier of modifiers) {
await this.pressKeyDown(modifier);
}
if (!this.browser.isW3C) {
await this.browser.sendKeys([key]);
} else {
await this.browser.performActions([{
type: 'key',
id: 'keyboard',
actions: [{
type: 'keyDown',
value: key,
}, {
type: 'keyUp',
value: key,
}],
}]);
}
for (const modifier of modifiers) {
await this.pressKeyUp(modifier);
}
}
/**
* {{> resizeWindow }}
* Appium: not tested in web, in apps doesn't work
*/
async resizeWindow(width, height) {
return this._resizeBrowserWindow(this.browser, width, height);
}
async _resizeBrowserWindow(browser, width, height) {
if (width === 'maximize') {
const size = await browser.maximizeWindow();
this.debugSection('Window Size', size);
return;
}
if (browser.isW3C) {
return browser.setWindowRect(null, null, parseInt(width, 10), parseInt(height, 10));
}
return browser.setWindowSize(parseInt(width, 10), parseInt(height, 10));
}
async _resizeWindowIfNeeded(browser, windowSize) {
if (this.isWeb && windowSize === 'maximize') {
await this._resizeBrowserWindow(browser, 'maximize');
} else if (this.isWeb && windowSize && windowSize.indexOf('x') > 0) {
const dimensions = windowSize.split('x');
await this._resizeBrowserWindow(browser, dimensions[0], dimensions[1]);
}
}
/**
* {{> dragAndDrop }}
* Appium: not tested
*/
async dragAndDrop(srcElement, destElement) {
let sourceEl = await this._locate(srcElement);
assertElementExists(sourceEl);
sourceEl = usingFirstElement(sourceEl);
let destEl = await this._locate(destElement);
assertElementExists(destEl);
destEl = usingFirstElement(destEl);
return sourceEl.dragAndDrop(destEl);
}
/**
* {{> dragSlider }}
*/
async dragSlider(locator, offsetX = 0) {
const browser = this.browser;
await this.moveCursorTo(locator);
// for chrome
if (browser.isW3C) {
return browser.performActions([
{ type: 'pointerDown', button: 0 },
{
type: 'pointerMove', origin: 'pointer', duration: 1000, x: offsetX, y: 0,
},
{ type: 'pointerUp', button: 0 },
]);
}
await browser.buttonDown(0);
await browser.moveToElement(null, offsetX, 0);
await browser.buttonUp(0);
}
/**
* Get all Window Handles.
* Useful for referencing a specific handle when calling `I.switchToWindow(handle)`
*
* ```js
* const windows = await I.grabAllWindowHandles();
* ```
*/
async grabAllWindowHandles() {
return this.browser.getWindowHandles();
}
/**
* Get the current Window Handle.
* Useful for referencing it when calling `I.switchToWindow(handle)`
*
* ```js
* const window = await I.grabCurrentWindowHandle();
* ```
*/
async grabCurrentWindowHandle() {
return this.browser.getWindowHandle();
}
/**
* Switch to the window with a specified handle.
*
* ```js
* const windows = await I.grabAllWindowHandles();
* // ... do something
* await I.switchToWindow( windows[0] );
*
* const window = await I.grabCurrentWindowHandle();
* // ... do something
* await I.switchToWindow( window );
* ```
*/
async switchToWindow(window) {
await this.browser.switchToWindow(window);
}
/**
* Close all tabs except for the current one.
*
*
* ```js
* I.closeOtherTabs();
* ```
*/
async closeOtherTabs() {
const handles = await this.browser.getWindowHandles();
const currentHandle = await this.browser.getWindowHandle();
const otherHandles = handles.filter(handle => handle !== currentHandle);
await forEachAsync(otherHandles, async (handle) => {
await this.browser.switchToWindow(handle);
await this.browser.closeWindow();
});
await this.browser.switchToWindow(currentHandle);
}
/**
* {{> wait }}
*
*/
async wait(sec) {
return new Promise(resolve => setTimeout(resolve, sec * 1000));
}
/**
* {{> waitForEnabled }}
*
*/
async waitForEnabled(locator, sec = null) {
const aSec = sec || this.options.waitForTimeout;
return this.browser.waitUntil(async () => {
const res = await this.$$(withStrictLocator(locator));
if (!res || res.length === 0) {
return false;
}
const selected = await forEachAsync(res, async el => this.browser.isElementEnabled(getElementId(el)));
if (Array.isArray(selected)) {
return selected.filter(val => val === true).length > 0;
}
return selected;
}, aSec * 1000, `element (${new Locator(locator)}) still not enabled after ${aSec} sec`);
}
/**
* {{> waitForElement }}
*/
async waitForElement(locator, sec = null) {
const aSec = sec || this.options.waitForTimeout;
return this.browser.waitUntil(async () => {
const res = await this.$$(withStrictLocator(locator));
return res && res.length;
}, aSec * 1000, `element (${locator}) still not present on page after ${aSec} sec`);
}
/**
* {{> waitForClickable }}
*/
async waitForClickable(locator, waitTimeout) {
waitTimeout = waitTimeout || this.options.waitForTimeout;
let res = awa