UNPKG

codeceptjs

Version:

Modern Era Acceptance Testing Framework for NodeJS

1,597 lines (1,431 loc) 67.2 kB
let webdriverio; const requireg = require('requireg'); const Helper = require('../helper'); const stringIncludes = require('../assert/include').includes; const { urlEquals, equals } = require('../assert/equal'); const empty = require('../assert/empty').empty; const truth = require('../assert/truth').truth; const { xpathLocator, fileExists, clearString, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, } = require('../utils'); const { isColorProperty, convertColorToRGBA, } = require('../colorUtils'); const ElementNotFound = require('./errors/ElementNotFound'); const ConnectionRefused = require('./errors/ConnectionRefused'); const assert = require('assert'); const path = require('path'); const webRoot = 'body'; const Locator = require('../locator'); let withinStore = {}; /** * WebDriverIO helper which wraps [webdriverio](http://webdriver.io/) library to * manipulate browser using Selenium WebDriver or PhantomJS. * * WebDriverIO 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["WebDriverIO"]._startBrowser()`. * * `timeouts`: [WebDriverIO timeouts](http://webdriver.io/guide/testrunner/timeouts.html) defined as hash. * * Example: * * ```json * { * "helpers": { * "WebDriverIO" : { * "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 * * ```json * { * "helpers": { * "WebDriverIO" : { * "url": "http://localhost", * "browser": "chrome", * "desiredCapabilities": { * "chromeOptions": { * "args": [ "--headless", "--disable-gpu", "--no-sandbox" ] * } * } * } * } * } * ``` * * ### 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.WebDriverIO.desiredCapabilities.proxy` key. * * ```js * { * "helpers": { * "WebDriverIO": { * "desiredCapabilities": { * "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": { * "WebDriverIO": { * "desiredCapabilities": { * "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 * * WebDriverIO 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. WebDriverIO automatically know which * service provider to connect to. * * ```js * { * "helpers":{ * "WebDriverIO": { * "url": "YOUR_DESIRED_HOST", * "user": "YOUR_BROWSERSTACK_USER", * "key": "YOUR_BROWSERSTACK_KEY", * "desiredCapabilities": { * "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, * } * } * } * } * ``` * * ### 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": { * "WebDriverIO": { * "multiremote": { * "MyChrome": { * "desiredCapabilities": { * "browserName": "chrome" * } * }, * "MyFirefox": { * "desiredCapabilities": { * "browserName": "firefox" * } * } * } * } * } * } * ``` * * * ## Access From Helpers * * Receive a WebDriverIO client from a custom helper by accessing `browser` property: * * ```js * this.helpers['WebDriverIO'].browser * ``` */ class WebDriverIO extends Helper { constructor(config) { super(config); webdriverio = requireg('webdriverio'); console.log('DEPRECATION WARNING', 'WebDriverIO helper is deprecated'); console.log('DEPRECATION WARNING', 'WebDriverIO was based on webdriverio package v4 which is outdated now'); console.log('DEPRECATION WARNING', 'Upgrade to "webdriverio@5" and switch to WebDriver helper'); // set defaults this.root = webRoot; this.isRunning = false; this._setConfig(config); } _validateConfig(config) { const defaults = { smartWait: 0, waitForTimeout: 1000, // ms desiredCapabilities: {}, 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); config.baseUrl = config.url || config.baseUrl; config.desiredCapabilities.browserName = config.browser || config.desiredCapabilities.browserName; config.waitForTimeout /= 1000; // convert to seconds if (!config.desiredCapabilities.platformName && (!config.url || !config.browser)) { throw new Error(` WebDriverIO requires at url and browser to be set. Check your codeceptjs config file to ensure these are set properly { "helpers": { "WebDriverIO": { "url": "YOUR_HOST" "browser": "YOUR_PREFERRED_TESTING_BROWSER" } } } `); } return config; } static _checkRequirements() { try { requireg('webdriverio'); } catch (e) { return ['webdriverio@4']; } } 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() { if (this.options.multiremote) { this.browser = webdriverio.multiremote(this.options.multiremote).init(); } else { this.browser = webdriverio.remote(this.options).init(); } try { await this.browser; } catch (err) { if (err.toString().indexOf('ECONNREFUSED')) { throw new ConnectionRefused(err); } throw err; } this.isRunning = true; if (this.options.timeouts) { await this.defineTimeout(this.options.timeouts); } if (this.options.windowSize === 'maximize') { await this.resizeWindow('maximize'); } else if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) { const dimensions = this.options.windowSize.split('x'); await this.resizeWindow(dimensions[0], dimensions[1]); } return this.browser; } async _stopBrowser() { if (this.browser && this.isRunning) await this.browser.end(); } 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(); return this.browser; } async _after() { if (!this.isRunning) return; if (this.options.restart) { this.isRunning = false; return this.browser.end(); } if (this.options.keepBrowserState) return; if (!this.options.keepCookies && this.options.desiredCapabilities.browserName) { this.debugSection('Session', 'cleaning cookies and localStorage'); await this.browser.deleteCookie(); } 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.requestHandler.sessionID; 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 = webdriverio.remote(opts).init(); await browser; return browser.requestHandler.sessionID; }, stop: async (sessionId) => { return this.browser.session('DELETE', sessionId); }, loadVars: async (sessionId) => { if (isWithin()) throw new Error('Can\'t start session inside within block'); this.browser.requestHandler.sessionID = sessionId; }, restoreVars: async () => { if (isWithin()) await this._withinEnd(); this.browser.requestHandler.sessionID = defaultSession; }, }; } async _failed(test) { if (isWithin()) await this._withinEnd(); } async _withinBegin(locator) { const frame = isFrameLocator(locator); const client = this.browser; if (frame) { if (Array.isArray(frame)) { withinStore.frame = frame.join('>'); return client .frame(null) .then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve())); } withinStore.frame = frame; return this.switchTo(frame); } withinStore.elFn = this.browser.element; withinStore.elsFn = this.browser.elements; this.context = locator; const res = await client.element(withStrictLocator.call(this, locator)); assertElementExists(res, locator); this.browser.element = function (l) { return this.elementIdElement(res.value.ELEMENT, l); }; this.browser.elements = function (l) { return this.elementIdElements(res.value.ELEMENT, l); }; return this.browser; } async _withinEnd() { if (withinStore.frame) { withinStore = {}; return this.switchTo(null); } this.context = this.root; this.browser.element = withinStore.elFn; this.browser.elements = withinStore.elsFn; withinStore = {}; } /** * Get elements by different locator types, including strict locator. * Should be used in custom helpers: * * ```js * this.helpers['WebDriverIO']._locate({name: 'password'}).then //... * ``` * * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. */ async _locate(locator, smartWait = false) { if (!this.options.smartWait || !smartWait) return this.browser.elements(withStrictLocator.call(this, locator)); this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`); await this.defineTimeout({ implicit: this.options.smartWait }); const els = await this.browser.elements(withStrictLocator.call(this, locator)); await this.defineTimeout({ implicit: 0 }); return els; } /** * Find a checkbox by providing human readable text: * * ```js * this.helpers['WebDriverIO']._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.browser.elements.bind(this)).then(res => res.value); } /** * Find a clickable element by providing human readable text: * * ```js * this.helpers['WebDriverIO']._locateClickable('Next page').then // ... * ``` * * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. */ async _locateClickable(locator) { return findClickable.call(this, locator, this.browser.elements.bind(this)).then(res => res.value); } /** * Find field elements by providing human readable text: * * ```js * this.helpers['WebDriverIO']._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.value); } /** * Set [WebDriverIO timeouts](http://webdriver.io/guide/testrunner/timeouts.html) in realtime. * Appium: support only web testing. * 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. */ async defineTimeout(timeouts) { try { // try to set W3C compatible timeouts await this.browser.timeouts(timeouts); } catch (error) { if (timeouts.implicit) { await this.browser.timeouts('implicit', timeouts.implicit); } if (timeouts['page load']) { await this.browser.timeouts('page load', timeouts['page load']); } // both pageLoad and page load are accepted // see http://webdriver.io/api/protocol/timeouts.html if (timeouts.pageLoad) { await this.browser.timeouts('page load', timeouts.pageLoad); } if (timeouts.script) { await this.browser.timeouts('script', timeouts.script); } } return this.browser; // return the last response } /** * {{> amOnPage }} * Appium: support only web testing */ amOnPage(url) { return this.browser.url(url); } /** * {{> click }} * Appium: support */ async click(locator, context = null) { const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick'; 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).toString()}`); } else { assertElementExists(res, locator, 'Clickable element'); } return this.browser[clickMethod](res.value[0].ELEMENT); } /** * {{> doubleClick }} * Appium: support only web testing */ async doubleClick(locator, context = null) { const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick'; 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).toString()}`); } else { assertElementExists(res, locator, 'Clickable element'); } const elem = res.value[0]; return this.browser.moveTo(elem.ELEMENT).doDoubleClick(); } /** * {{> rightClick }} * Appium: support, but in apps works as usual click */ async rightClick(locator) { // just press button if no selector is given if (locator === undefined) { return this.browser.buttonPress('right'); } const res = await this._locate(locator, true); assertElementExists(res, locator, 'Clickable element'); const elem = res.value[0]; if (this.browser.isMobile) return this.browser.touchClick(elem.ELEMENT); return this.browser.moveTo(elem.ELEMENT).buttonPress('right'); } /** * {{> fillField }} * Appium: support */ async fillField(field, value) { const res = await findFields.call(this, field); assertElementExists(res, field, 'Field'); const elem = res.value[0]; return this.browser.elementIdClear(elem.ELEMENT).elementIdValue(elem.ELEMENT, value.toString()); } /** * {{> appendField }} * Appium: support, but it's clear a field before insert in apps */ async appendField(field, value) { const res = await findFields.call(this, field); assertElementExists(res, field, 'Field'); const elem = res.value[0]; return this.browser.elementIdValue(elem.ELEMENT, value); } /** * {{> clearField}} * Appium: support */ async clearField(field) { const res = await findFields.call(this, field); assertElementExists(res, field, 'Field'); const elem = res.value[0]; return this.browser.elementIdClear(elem.ELEMENT); } /** * {{> selectOption}} */ async selectOption(select, option) { const res = await findFields.call(this, select); assertElementExists(res, select, 'Selectable field'); const elem = res.value[0]; if (!Array.isArray(option)) { option = [option]; } // select options by visible text let els = await forEachAsync(option, async opt => this.browser.elementIdElements(elem.ELEMENT, Locator.select.byVisibleText(xpathLocator.literal(opt)))); const clickOptionFn = async (el) => { if (el[0]) el = el[0]; if (el && el.ELEMENT) return this.browser.elementIdClick(el.ELEMENT); }; if (Array.isArray(els) && els.length) { return forEachAsync(els, clickOptionFn); } // select options by value els = await forEachAsync(option, async opt => this.browser.elementIdElements(elem.ELEMENT, Locator.select.byValue(xpathLocator.literal(opt)))); if (els.length === 0) { throw new ElementNotFound(select, `Option ${option} in`, 'was found neither by visible text not by value'); } return forEachAsync(els, clickOptionFn); } /** * {{> attachFile }} * Appium: not tested */ async attachFile(locator, pathToFile) { const file = path.join(global.codecept_dir, pathToFile); if (!fileExists(file)) { throw new Error(`File at ${file} can not be found on local system`); } const el = await findFields.call(this, locator); this.debug(`Uploading ${file}`); const res = await this.browser.uploadFile(file); assertElementExists(el, locator, 'File field'); return this.browser.elementIdValue(el.value[0].ELEMENT, res.value); } /** * {{> checkOption }} * Appium: not tested */ async checkOption(field, context = null) { const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick'; const locateFn = prepareLocateFn.call(this, context); const res = await findCheckable.call(this, field, locateFn); assertElementExists(res, field, 'Checkable'); const elem = res.value[0]; const isSelected = await this.browser.elementIdSelected(elem.ELEMENT); if (isSelected.value) return Promise.resolve(true); return this.browser[clickMethod](elem.ELEMENT); } /** * {{> uncheckOption }} * Appium: not tested */ async uncheckOption(field, context = null) { const clickMethod = this.browser.isMobile ? 'touchClick' : 'elementIdClick'; const locateFn = prepareLocateFn.call(this, context); const res = await findCheckable.call(this, field, locateFn); assertElementExists(res, field, 'Checkable'); const elem = res.value[0]; const isSelected = await this.browser.elementIdSelected(elem.ELEMENT); if (!isSelected.value) return Promise.resolve(true); return this.browser[clickMethod](elem.ELEMENT); } /** * {{> grabTextFrom }} * Appium: support */ async grabTextFrom(locator) { const res = await this._locate(locator, true); assertElementExists(res, locator); const val = await forEachAsync(res.value, async el => this.browser.elementIdText(el.ELEMENT)); this.debugSection('Grab', val); return val; } /** * {{> grabHTMLFrom }} * Appium: support only web testing */ async grabHTMLFrom(locator) { const html = await this.browser.getHTML(withStrictLocator.call(this, locator)); this.debugSection('Grab', html); return html; } /** * {{> grabValueFrom }} * Appium: support only web testing */ async grabValueFrom(locator) { const res = await this._locate(locator, true); assertElementExists(res, locator); return forEachAsync(res.value, async el => this.browser.elementIdAttribute(el.ELEMENT, 'value')); } /** * {{> grabCssPropertyFrom }} */ async grabCssPropertyFrom(locator, cssProperty) { const res = await this._locate(locator, true); assertElementExists(res, locator); return forEachAsync(res.value, async el => this.browser.elementIdCssProperty(el.ELEMENT, 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.value, async el => this.browser.elementIdAttribute(el.ELEMENT, attr)); } /** * {{> seeInTitle }} * Appium: support only web testing */ 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 }} * Appium: support only web testing */ async dontSeeInTitle(text) { const title = await this.browser.getTitle(); return stringIncludes('web page title').negate(text, title); } /** * {{> grabTitle }} * Appium: support only web testing */ async grabTitle() { const title = await this.browser.getTitle(); this.debugSection('Title', title); return title; } /** * {{> see }} * Appium: support with context in apps */ 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 }} * Appium: support with context in apps */ async dontSee(text, context = null) { return proceedSee.call(this, 'negate', text, context); } /** * {{> seeInField }} * Appium: support only web testing */ async seeInField(field, value) { return proceedSeeField.call(this, 'assert', field, value); } /** * {{> dontSeeInField }} * Appium: support only web testing */ 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 }} * Appium: support */ async seeElement(locator) { const res = await this._locate(locator, true); if (!res.value || res.value.length === 0) { return truth(`elements of ${locator}`, 'to be seen').assert(false); } const selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT)); return truth(`elements of ${locator}`, 'to be seen').assert(selected); } /** * {{> dontSeeElement}} * Appium: support */ async dontSeeElement(locator) { const res = await this._locate(locator, false); if (!res.value || res.value.length === 0) { return truth(`elements of ${locator}`, 'to be seen').negate(false); } const selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT)); return truth(`elements of ${locator}`, 'to be seen').negate(selected); } /** * {{> seeElementInDOM }} * Appium: support */ async seeElementInDOM(locator) { const res = await this.browser.elements(withStrictLocator.call(this, locator)); return empty('elements').negate(res.value); } /** * {{> dontSeeElementInDOM }} * Appium: support */ async dontSeeElementInDOM(locator) { const res = await this.browser.elements(withStrictLocator.call(this, locator)); return empty('elements').assert(res.value); } /** * {{> seeInSource }} * Appium: support */ async seeInSource(text) { const source = await this.browser.getSource(); return stringIncludes('HTML source of a page').assert(text, source); } /** * {{> grabSource }} * Appium: support */ async grabSource() { return this.browser.getSource(); } /** * Get JS log from browser. Log buffer is reset after each request. * * ```js * let logs = await I.grabBrowserLogs(); * console.log(JSON.stringify(logs)) * ``` */ async grabBrowserLogs() { return this.browser.log('browser').then(res => res.value); } /** * {{> grabCurrentUrl }} */ async grabCurrentUrl() { const res = await this.browser.url(); return res.value; } async grabBrowserUrl() { console.log('grabBrowserUrl deprecated. Use grabCurrentUrl instead'); const res = await this.browser.url(); return res.value; } /** * {{> dontSeeInSource }} * Appium: support */ async dontSeeInSource(text) { const source = await this.browser.getSource(); return stringIncludes('HTML source of a page').negate(text, source); } /** * Asserts that an element appears a given number of times in the DOM. * Element is located by label or name or CSS or XPath. * Appium: support * * ```js * I.seeNumberOfElements('#submitBtn', 1); * ``` * * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. * @param {number} [num] number of elements. */ async seeNumberOfElements(locator, num) { const res = await this._locate(withStrictLocator.call(this, locator)); return assert.equal(res.value.length, num, `expected number of elements (${locator}) is ${num}, but found ${res.value.length}`); } /** * {{> seeNumberOfVisibleElements }} */ 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.value.length; let props = await forEachAsync(res.value, async (el) => { return forEachAsync(Object.keys(cssProperties), async (prop) => { const propValue = await this.browser.elementIdCssProperty(el.ELEMENT, 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.value.length; let attrs = await forEachAsync(res.value, async (el) => { return forEachAsync(Object.keys(attributes), async attr => this.browser.elementIdAttribute(el.ELEMENT, 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.value, async el => this.browser.elementIdDisplayed(el.ELEMENT)); if (!Array.isArray(selected)) selected = [selected]; selected = selected.filter(val => val === true); return selected.length; } /** * {{> seeInCurrentUrl }} * Appium: support only web testing */ async seeInCurrentUrl(url) { const res = await this.browser.url(); return stringIncludes('url').assert(url, decodeUrl(res.value)); } /** * {{> dontSeeInCurrentUrl }} * Appium: support only web testing */ async dontSeeInCurrentUrl(url) { const res = await this.browser.url(); return stringIncludes('url').negate(url, decodeUrl(res.value)); } /** * {{> seeCurrentUrlEquals }} * Appium: support only web testing */ async seeCurrentUrlEquals(url) { const res = await this.browser.url(); return urlEquals(this.options.url).assert(url, decodeUrl(res.value)); } /** * {{> dontSeeCurrentUrlEquals }} * Appium: support only web testing */ async dontSeeCurrentUrlEquals(url) { const res = await this.browser.url(); return urlEquals(this.options.url).negate(url, decodeUrl(res.value)); } /** * {{> executeScript }} * Appium: support only web testing * * Wraps [execute](http://webdriver.io/api/protocol/execute.html) command. */ executeScript(fn) { return this.browser.execute.apply(this.browser, arguments).then(res => res.value); } /** * {{> executeAsyncScript }} * Appium: support only web testing */ executeAsyncScript(fn) { return this.browser.executeAsync.apply(this.browser, arguments).then(res => res.value); } /** * Scrolls to element matched by locator. * Extra shift can be set with offsetX and offsetY options. * * ```js * I.scrollTo('footer'); * I.scrollTo('#submit', 5, 5); * ``` * * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator. * @param {number} [offsetX=0] (optional) X-axis offset. * @param {number} [offsetY=0] (optional) Y-axis offset. */ /** * {{> scrollTo }} * Appium: support only web testing */ 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.call(this, locator), true); if (!res.value || res.value.length === 0) { return truth(`elements of ${locator}`, 'to be seen').assert(false); } const elem = res.value[0]; if (this.browser.isMobile) return this.browser.touchScroll(elem.ELEMENT, offsetX, offsetY); const location = await this.browser.elementIdLocation(elem.ELEMENT); assertElementExists(location, 'Failed to receive', 'location'); /* eslint-disable prefer-arrow-callback */ return this.browser.execute(function (x, y) { return window.scrollTo(x, y); }, location.value.x + offsetX, location.value.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}} * Appium: support only web testing */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { let hasOffsetParams = true; if (typeof offsetX !== 'number' && typeof offsetY !== 'number') { hasOffsetParams = false; } const res = await this._locate(withStrictLocator.call(this, locator), true); assertElementExists(res, locator); const elem = res.value[0]; if (!this.browser.isMobile) { return this.browser.moveTo(elem.ELEMENT, offsetX, offsetY); } const size = await this.browser.elementIdSize(elem.ELEMENT); assertElementExists(size, 'Failed to receive', 'size'); const location = await this.browser.elementIdLocation(elem.ELEMENT); assertElementExists(size, 'Failed to receive', 'location'); let x = location.value.x + size.value.width / 2; let y = location.value.y + size.value.height / 2; if (hasOffsetParams) { x = location.value.x + offsetX; y = location.value.y + offsetY; } return this.browser.touchMove(x, y); } /** * {{> saveScreenshot}} * Appium: support */ 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 */ let { width, height } = await this.browser.execute(function () { return { height: document.body.scrollHeight, width: document.body.scrollWidth }; }).then(res => res.value); if (height < 100) height = 500; // errors for very small height /* eslint-enable */ await this.browser.windowHandleSize({ width, height }); this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`); return this.browser.saveScreenshot(outputFile); } /** * {{> setCookie}} * Appium: support only web testing * * Uses Selenium's JSON [cookie * format](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object). */ async setCookie(cookie) { return this.browser.setCookie(cookie); } /** * {{> clearCookie}} * Appium: support only web testing */ async clearCookie(cookie) { return this.browser.deleteCookie(cookie); } /** * {{> seeCookie}} * Appium: support only web testing */ async seeCookie(name) { const res = await this.browser.getCookie(name); return truth(`cookie ${name}`, 'to be set').assert(res); } /** * {{> dontSeeCookie}} * Appium: support only web testing */ async dontSeeCookie(name) { const res = await this.browser.getCookie(name); return truth(`cookie ${name}`, 'to be set').negate(res); } /** * {{> grabCookie}} * Appium: support only web testing */ async grabCookie(name) { return this.browser.getCookie(name); } /** * 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). Appium: support only web testing */ async acceptPopup() { return this.browser.alertText().then(function (res) { if (res !== null) { return this.alertAccept(); } }); } /** * Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt. * Appium: support only web testing */ async cancelPopup() { return this.browser.alertText().then(function (res) { if (res !== null) { return this.alertDismiss(); } }); } /** * Checks that the active JavaScript popup, as created by `window.alert|window.confirm|window.prompt`, contains the * given string. Appium: support only web testing * * @param {string} text value to check. */ async seeInPopup(text) { return this.browser.alertText().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() { return this.browser.alertText() .catch(() => null); // Don't throw an error } /** * {{> pressKeyWithKeyNormalization }} */ async pressKey(key) { let modifier; const modifiers = ['Control', 'Command', 'Shift', 'Alt']; if (Array.isArray(key) && modifiers.indexOf(key[0]) > -1) { modifier = key[0]; } await this.browser.keys(key); if (!modifier) return true; return this.browser.keys(modifier); // release modifier } /** * {{> resizeWindow }} * Appium: not tested in web, in apps doesn't work */ async resizeWindow(width, height) { if (width !== 'maximize') { return this.browser.windowHandleSize({ width, height }); } /* eslint-disable prefer-arrow-callback,comma-dangle */ const { screenWidth, screenHeight } = await this.browser.execute(function () { return { screenHeight: window.screen.height, screenWidth: window.screen.width }; }).then(res => res.value); /* eslint-enable prefer-arrow-callback,comma-dangle */ return this.browser.windowHandleSize({ width: screenWidth, height: screenHeight }); } /** * {{> dragAndDrop }} * Appium: not tested */ async dragAndDrop(srcElement, destElement) { const client = this.browser; if (client.isMobile) { let res = await this._locate(withStrictLocator.call(this, srcElement), true); assertElementExists(res, srcElement); let elem = res.value[0]; let location = await this.browser.elementIdLocation(elem.ELEMENT); assertElementExists(location, `Failed to receive (${srcElement}) location`); res = await this.browser.touchDown(location.value.x, location.value.y); if (res.state !== 'success') throw new Error(`Failed to touch button down on (${srcElement})`); res = await this._locate(withStrictLocator.call(this, destElement), true); assertElementExists(res, destElement); elem = res.value[0]; location = await this.browser.elementIdLocation(elem.ELEMENT); assertElementExists(location, `Failed to receive (${destElement}) location`); res = await this.browser.touchMove(location.value.x, location.value.y); if (res.state !== 'success') throw new Error(`Failed to touch move to (${destElement})`); return this.browser.touchUp(location.value.x, location.value.y); } return client.dragAndDrop( withStrictLocator.call(this, srcElement), withStrictLocator.call(this, destElement), ); } /** * Close all tabs except for the current one. * Appium: support web test * * ```js * I.closeOtherTabs(); * ``` */ async closeOtherTabs() { const client = this.browser; const handles = await client.getTabIds(); const currentHandle = await client.getCurrentTabId(); const otherHandles = handles.filter(handle => handle !== currentHandle); let p = Promise.resolve(); otherHandles.forEach((handle) => { p = p.then(() => client.switchTab(handle).then(() => client.close(currentHandle))); }); return p; } /** * {{> wait }} * Appium: support */ async wait(sec) { return this.browser.pause(sec * 1000); } /** * {{> waitForEnabled }} * Appium: support */ async waitForEnabled(locator, sec = null) { const client = this.browser; const aSec = sec || this.options.waitForTimeout; return client.waitUntil(async () => { const res = await this.browser.elements(withStrictLocator.call(this, locator)); if (!res.value || res.value.length === 0) { return false; } const selected = await forEachAsync(res.value, async el => client.elementIdEnabled(el.ELEMENT)); if (Array.isArray(selected)) { return selected.filter(val => val === true).length > 0; } return selected; }, aSec * 1000, `element (${JSON.stringify(locator)}) still not enabled after ${aSec} sec`); } /** * {{> waitForElement }} * Appium: support */ async waitForElement(locator, sec = null) { const aSec = sec || this.options.waitForTimeout; return this.browser.waitUntil(async () => { const res = await this.browser.elements(withStrictLocator.call(this, locator)); return res.value && res.value.length; }, aSec * 1000, `element (${locator}) still not present on page after ${aSec} sec`); } async waitUntilExists(locator, sec = null) { console.log(`waitUntilExists deprecated: * use 'waitForElement' to wait for element to be attached * use 'waitForDetached to wait for element to be removed'`); return this.waitForStalenessOf(locator, sec); } /** * {{> waitInUrl }} */ async waitInUrl(urlPart, sec = null) { const client = this.browser; const aSec = sec || this.options.waitForTimeout; let currUrl = ''; return client .waitUntil(function () { return this.url().then((res) => { currUrl = decodeUrl(res.value); return currUrl.indexOf(urlPart) > -1; }); }, aSec * 1000).catch((e) => { if (e.type === 'WaitUntilTimeoutError') { throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`); } else { throw e; } }); } /** * {{> waitUrlEquals }} */ async waitUrlEquals(urlPart, sec = null) { const aSec = sec || this.options.waitForTimeout; const baseUrl = this.options.url; if (urlPart.indexOf('http') < 0) { urlPart = baseUrl + urlPart; } let currUrl = ''; return this.browser.waitUntil(function () { return this.url().then((res) => { currUrl = decodeUrl(res.value); return currUrl === urlPart; }); }, aSec * 1000).catch((e) => { if (e.type === 'WaitUntilTimeoutError') { throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`); } else { throw e; } }); } /** * {{> waitForText }} * Appium: support */ async waitForText(text, sec = null, context = null) { const aSec = sec || this.options.waitForTimeout; const _context = context || this.root; return this.browser.waitUntil( async () => { const res = await this.browser.elements(withStrictLocator.call(this, _context)); if (!res.value || res.value.length === 0) return false; const selected = await forEachAsync(res.value, async el => this.browser.elementIdText(el.ELEMENT)); if (Array.isArray(selected)) { return selected.filter(part => part.indexOf(text) >= 0).length > 0; } return selected.indexOf(text) >= 0; }, aSec * 1000, `element (${_context}) is not in DOM or there is no element(${_context}) with text "${text}" after ${aSec} sec`, ); } /** * {{> waitForValue }} */ async waitForValue(field, value, sec = null) { const client = this.browser; const aSec = sec || this.options.waitForTimeout; return client.waitUntil( async () => { const res = await findFields.call(this, field); if (!res.value || res.value.length === 0) return false; const selected = await forEachAsync(res.value, async el => this.browser.elementIdAttribute(el.ELEMENT, 'value')); if (Array.isArray(selected)) { return selected.filter(part => part.indexOf(value) >= 0).length > 0; } return selected.indexOf(value) >= 0; }, aSec * 1000, `element (${field}) is not in DOM or there is no element(${field}) with value "${value}" after ${aSec} sec`, ); } /** * {{> waitForVisible }} * Appium: support */ async waitForVisible(locator, sec = null) { const aSec = sec || this.options.waitForTimeout; return this.browser.waitUntil(async () => { const res = await this.browser.elements(withStrictLocator.call(this, locator)); if (!res.value || res.value.length === 0) return false; const selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT)); if (Array.isArray(selected)) { return selected.filter(val => val === true).length > 0; } return selected; }, aSec * 1000, `element (${JSON.stringify(locator)}) still not visible after ${aSec} sec`); } /** * {{> waitNumberOfVisibleElements }} */ async waitNumberOfVisibleElements(locator, num, sec = null) { const aSec = sec || this.options.waitForTimeout; return this.browser.waitUntil(async () => { const res = await this.browser.elements(withStrictLocator.call(this, locator)); if (!res.value || res.value.length === 0) return false; let selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT)); if (!Array.isArray(selected)) selected = [selected]; return selected.length === num; }, aSec * 1000, `The number of elements (${JSON.stringify(locator)}) is not ${num} after ${aSec} sec`); } /** * {{> waitForInvisible }} * Appium: support */ async waitForInvisible(locator, sec = null) { const aSec = sec || this.options.waitForTimeout; return this.browser.waitUntil(async () => { const res = await this.browser.elements(withStrictLocator.call(this, locator)); if (!res.value || res.value.length === 0) return true; const selected = await forEachAsync(res.value, async el => this.browser.elementIdDisplayed(el.ELEMENT