UNPKG

codeceptjs

Version:

Modern Era Acceptance Testing Framework for NodeJS

1,746 lines (1,584 loc) 75.8 kB
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