UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

1,642 lines (1,484 loc) 91.6 kB
let webdriverio const assert = require('assert') const path = require('path') const Helper = require('@codeceptjs/helper') const promiseRetry = require('promise-retry') const stringIncludes = require('../assert/include').includes const { urlEquals, equals } = require('../assert/equal') const store = require('../store') const { debug } = require('../output') const { empty } = require('../assert/empty') const { truth } = require('../assert/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 { highlightElement } = require('./scripts/highlightElement') const { focusElement } = require('./scripts/focusElement') const { blurElement } = require('./scripts/blurElement') const { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } = require('./errors/ElementAssertion') const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions') const SHADOW = 'shadow' const webRoot = 'body' let browserLogs = [] /** * ## Configuration * * This helper should be configured in codecept.conf.js * * @typedef WebDriverConfig * @type {object} * @prop {string} url - base url of website to be tested. * @prop {string} browser - Browser in which to perform testing. * @prop {boolean} [bidiProtocol=false] - WebDriver Bidi Protocol. Default: false. More info: https://webdriver.io/docs/api/webdriverBidi/ * @prop {string} [basicAuth] - (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'} * @prop {string} [host=localhost] - WebDriver host to connect. * @prop {number} [port=4444] - WebDriver port to connect. * @prop {string} [protocol=http] - protocol for WebDriver server. * @prop {string} [path=/wd/hub] - path to WebDriver server. * @prop {boolean} [restart=true] - restart browser between tests. * @prop {boolean|number} [smartWait=false] - **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000. * @prop {boolean} [disableScreenshots=false] - don't save screenshots on failure. * @prop {boolean} [fullPageScreenshots=false] (optional - make full page screenshots on failure. * @prop {boolean} [uniqueScreenshotNames=false] - option to prevent screenshot override if you have scenarios with the same name in different suites. * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to false. * @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` set to false. * @prop {string} [windowSize=window] default window size. Set to `maximize` or a dimension in the format `640x480`. * @prop {number} [waitForTimeout=1000] sets default wait time in *ms* for all `wait*` functions. * @prop {object} [desiredCapabilities] Selenium's [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). * @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["WebDriver"]._startBrowser()`. * @prop {object} [timeouts] [WebDriver timeouts](http://webdriver.io/docs/timeouts.html) defined as hash. * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose). * @prop {string} [logLevel=silent] - level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: https://webdriver.io/docs/configuration/#loglevel */ const config = {} /** * 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. Those tools can be easily installed via NPM. Please check [Testing with WebDriver](https://codecept.io/webdriver/#testing-with-webdriver) for more details. * * With the release of WebdriverIO version v8.14.0, and onwards, all driver management hassles are now a thing of the past 🙌. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/). * One of the significant advantages of this update is that you can now get rid of any driver services you previously had to manage, such as * `wdio-chromedriver-service`, `wdio-geckodriver-service`, `wdio-edgedriver-service`, `wdio-safaridriver-service`, and even `@wdio/selenium-standalone-service`. * * For those who require custom driver options, fear not; WebDriver Helper allows you to pass in driver options through custom WebDriver configuration. * If you have a custom grid, use a cloud service, or prefer to run your own driver, there's no need to worry since WebDriver Helper will only start a driver when there are no other connection information settings like hostname or port specified. * * <!-- configuration --> * * Example: * * ```js * { * helpers: { * WebDriver : { * smartWait: 5000, * browser: "chrome", * restart: false, * windowSize: "maximize", * timeouts: { * "script": 60000, * "page load": 10000 * } * } * } * } * ``` * * Testing Chrome locally is now more convenient than ever. You can define a browser channel, and WebDriver Helper will take care of downloading the specified browser version for you. * For example: * * ```js * { * helpers: { * WebDriver : { * smartWait: 5000, * browser: "chrome", * browserVersion: '116.0.5793.0', // or 'stable', 'beta', 'dev' or 'canary' * restart: false, * windowSize: "maximize", * timeouts: { * "script": 60000, * "page load": 10000 * } * } * } * } * ``` * * * Example with basic authentication * ```js * { * helpers: { * WebDriver : { * smartWait: 5000, * browser: "chrome", * basicAuth: {username: 'username', password: 'password'}, * 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" ] * } * } * } * } * } * ``` * * ### Running with devtools protocol * * ```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 = require('webdriverio') // set defaults this.root = webRoot this.isWeb = true this.isRunning = false this.sessionWindows = {} this.activeSessionName = '' this.customLocatorStrategies = config.customLocatorStrategies // for network stuff this.requests = [] this.recording = false this.recordedAtLeastOnce = 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, highlightElement: false, } // override defaults with config config = Object.assign(defaults, config) if (config.host) { // webdriverio spec config.hostname = config.host config.path = config.path ? config.path : '/wd/hub' } 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 // WebDriver Bidi Protocol. Default: false config.capabilities.webSocketUrl = config.bidiProtocol ?? config.capabilities.webSocketUrl ?? true config.capabilities.browserVersion = config.browserVersion || config.capabilities.browserVersion 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.waitForTimeoutInSeconds = 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 { require('webdriverio') } catch (e) { return ['webdriverio@^6.12.1'] } } 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() } } _lookupCustomLocator(customStrategy) { if (typeof this.customLocatorStrategies !== 'object') { return null } const strategy = this.customLocatorStrategies[customStrategy] return typeof strategy === 'function' ? strategy : null } _isCustomLocator(locator) { const locatorObj = new Locator(locator) if (locatorObj.isCustom()) { const customLocator = this._lookupCustomLocator(locatorObj.type) if (customLocator) { return true } throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".') } return false } async _res(locator) { const res = this._isShadowLocator(locator) || this._isCustomLocator(locator) ? await this._locate(locator) : await this.$$(withStrictLocator(locator)) return res } async _startBrowser() { try { if (this.options.multiremote) { this.browser = await webdriverio.multiremote(this.options.multiremote) } else { // remove non w3c capabilities delete this.options.capabilities.protocol delete this.options.capabilities.hostname delete this.options.capabilities.port delete this.options.capabilities.path 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) if (this._isCustomLocatorStrategyDefined()) { Object.keys(this.customLocatorStrategies).forEach(async customLocator => { this.debugSection('Weddriver', `adding custom locator strategy: ${customLocator}`) const locatorFunction = this._lookupCustomLocator(customLocator) this.browser.addLocatorStrategy(customLocator, locatorFunction) }) } if (this.browser.capabilities && this.browser.capabilities.platformName) { this.browser.capabilities.platformName = this.browser.capabilities.platformName.toLowerCase() } this.browser.on('dialog', () => {}) await this.browser.sessionSubscribe({ events: ['log.entryAdded'] }) this.browser.on('log.entryAdded', logEvents) return this.browser } _isCustomLocatorStrategyDefined() { return this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length } 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() if (this.browser) 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.switchFrame(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() browserLogs = [] return this.browser } _afterSuite() {} _finishTest() { if (!this.options.restart && this.isRunning) return this._stopBrowser() } _session() { const defaultSession = this.browser return { start: async (sessionName, 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) this.activeSessionName = sessionName if (opts.timeouts && this.isWeb) { await this._defineBrowserTimeout(browser, opts.timeouts) } await this._resizeWindowIfNeeded(browser, opts.windowSize) return browser }, stop: async browser => { if (!browser) return 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) this.sessionWindows[this.activeSessionName] = browser }, restoreVars: async session => { if (!session) { this.activeSessionName = '' } this.browser = defaultSession this.$$ = this.browser.$$.bind(this.browser) }, } } /** * Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test. * * First argument is a description of an action. * Second argument is async function that gets this helper as parameter. * * { [`browser`](https://webdriver.io/docs/api.html)) } object from WebDriver API is available. * * ```js * I.useWebDriverTo('open multiple windows', async ({ browser }) { * // create new window * await browser.newWindow('https://webdriver.io'); * }); * ``` * * @param {string} description used to show in logs. * @param {function} fn async functuion that executed with WebDriver helper as argument */ useWebDriverTo(description, fn) { return this._useTo(...arguments) } async _failed() { 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) } /** * Check if locator is type of "Shadow" * * @param {object} locator */ _isShadowLocator(locator) { return locator.type === SHADOW || locator[SHADOW] } /** * Locate Element within the Shadow Dom * * @param {object} locator */ async _locateShadow(locator) { const shadow = locator.value ? locator.value : locator[SHADOW] const shadowSequence = [] let elements if (!Array.isArray(shadow)) { throw new Error(`Shadow '${shadow}' should be defined as an Array of elements.`) } // traverse through the Shadow locators in sequence for (let index = 0; index < shadow.length; index++) { const shadowElement = shadow[index] shadowSequence.push(shadowElement) if (!elements) { elements = await this.browser.$$(shadowElement) } else if (Array.isArray(elements)) { elements = await elements[0].shadow$$(shadowElement) } else if (elements) { elements = await elements.shadow$$(shadowElement) } if (!elements || !elements[0]) { throw new Error( `Shadow Element '${shadowElement}' is not found. It is possible the element is incorrect or elements sequence is incorrect. Please verify the sequence '${shadowSequence.join('>')}' is correctly chained.`, ) } } this.debugSection('Elements', `Found ${elements.length} '${SHADOW}' elements`) return elements } /** * Smart Wait to locate an element * * @param {object} locator */ async _smartWait(locator) { this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${JSON.stringify(locator)} in ${this.options.smartWait}`) await this.defineTimeout({ implicit: this.options.smartWait }) } /** * 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 Shadow DOM if (this._isShadowLocator(locator)) { if (!this.options.smartWait || !smartWait) { const els = await this._locateShadow(locator) return els } const els = await this._locateShadow(locator) return els } // 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) { if (this._isCustomLocator(locator)) { const locatorObj = new Locator(locator) return this.browser.custom$$(locatorObj.type, locatorObj.value) } const els = await this.$$(withStrictLocator(locator)) return els } await this._smartWait(locator) if (this._isCustomLocator(locator)) { const locatorObj = new Locator(locator) return this.browser.custom$$(locatorObj.type, locatorObj.value) } const els = await this.$$(withStrictLocator(locator)) await this.defineTimeout({ implicit: 0 }) return els } _grabCustomLocator(locator) { if (typeof locator === 'string') { locator = new Locator(locator) } return locator.value ? locator.value : locator.custom } /** * 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 * const els = await this.helpers.WebDriver._locateClickable('Next page'); * const els = await this.helpers.WebDriver._locateClickable('Next page', '.pages'); * ``` * * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. */ async _locateClickable(locator, context) { const locateFn = prepareLocateFn.call(this, context) return findClickable.call(this, locator, locateFn) } /** * 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) } /** * {{> grabWebElements }} * */ async grabWebElements(locator) { return this._locate(locator) } /** * 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 {*} timeouts WebDriver timeouts object. */ defineTimeout(timeouts) { return this._defineBrowserTimeout(this.browser, timeouts) } _defineBrowserTimeout(browser, timeouts) { return browser.setTimeout(timeouts) } /** * {{> amOnPage }} * */ amOnPage(url) { let split_url if (this.options.basicAuth) { if (url.startsWith('/')) { url = this.options.url + url } split_url = url.split('//') url = `${split_url[0]}//${this.options.basicAuth.username}:${this.options.basicAuth.password}@${split_url[1]}` } return this.browser.url(url) } /** * {{> click }} * * {{ react }} */ async click(locator, context = null) { const clickMethod = this.browser.isMobile && !this.browser.isW3C ? '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) highlightActiveElement.call(this, elem) return this.browser[clickMethod](getElementId(elem)) } /** * {{> forceClick }} * * {{ react }} */ async forceClick(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) highlightActiveElement.call(this, elem) return this.executeScript(el => { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } const event = document.createEvent('MouseEvent') event.initEvent('click', true, true) return el.dispatchEvent(event) }, 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) highlightActiveElement.call(this, elem) 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) { return el.click({ button: 'right' }) } // JSON Wire version await this.browser.buttonDown(2) } /** * {{> forceRightClick }} * * {{ react }} */ async forceRightClick(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 this.executeScript(el => { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } const event = document.createEvent('MouseEvent') event.initEvent('contextmenu', true, true) return el.dispatchEvent(event) }, elem) } /** * {{> fillField }} * {{ react }} * {{ custom }} * */ async fillField(field, value) { const res = await findFields.call(this, field) assertElementExists(res, field, 'Field') const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) await elem.clearValue() await 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) highlightActiveElement.call(this, elem) return elem.addValue(value.toString()) } /** * {{> clearField }} * */ async clearField(field) { const res = await findFields.call(this, field) assertElementExists(res, field, 'Field') const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) 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) highlightActiveElement.call(this, elem) 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) } /** * Appium: not tested * * {{> attachFile }} */ 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) } /** * Appium: not tested * {{> checkOption }} */ async checkOption(field, context = null) { const clickMethod = this.browser.isMobile && !this.browser.isW3C ? '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) highlightActiveElement.call(this, elem) const isSelected = await this.browser.isElementSelected(elementId) if (isSelected) return Promise.resolve(true) return this.browser[clickMethod](elementId) } /** * Appium: not tested * {{> uncheckOption }} */ async uncheckOption(field, context = null) { const clickMethod = this.browser.isMobile && !this.browser.isW3C ? '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) highlightActiveElement.call(this, elem) const isSelected = await this.browser.isElementSelected(elementId) if (!isSelected) return Promise.resolve(true) return this.browser[clickMethod](elementId) } /** * {{> grabTextFromAll }} * */ async grabTextFromAll(locator) { const res = await this._locate(locator, true) let val = [] await forEachAsync(res, async el => { const text = await this.browser.getElementText(getElementId(el)) val.push(text) }) this.debugSection('GrabText', String(val)) return val } /** * {{> grabTextFrom }} * */ async grabTextFrom(locator) { const texts = await this.grabTextFromAll(locator) assertElementExists(texts, locator) if (texts.length > 1) { this.debugSection('GrabText', `Using first element out of ${texts.length}`) } return texts[0] } /** * {{> grabHTMLFromAll }} * */ async grabHTMLFromAll(locator) { const elems = await this._locate(locator, true) const html = await forEachAsync(elems, elem => elem.getHTML(false)) this.debugSection('GrabHTML', String(html)) return html } /** * {{> grabHTMLFrom }} * */ async grabHTMLFrom(locator) { const html = await this.grabHTMLFromAll(locator) assertElementExists(html, locator) if (html.length > 1) { this.debugSection('GrabHTML', `Using first element out of ${html.length}`) } return html[0] } /** * {{> grabValueFromAll }} * */ async grabValueFromAll(locator) { const res = await this._locate(locator, true) const val = await forEachAsync(res, el => el.getValue()) this.debugSection('GrabValue', String(val)) return val } /** * {{> grabValueFrom }} * */ async grabValueFrom(locator) { const values = await this.grabValueFromAll(locator) assertElementExists(values, locator) if (values.length > 1) { this.debugSection('GrabValue', `Using first element out of ${values.length}`) } return values[0] } /** * {{> grabCssPropertyFromAll }} */ async grabCssPropertyFromAll(locator, cssProperty) { const res = await this._locate(locator, true) const val = await forEachAsync(res, async el => this.browser.getElementCSSValue(getElementId(el), cssProperty)) this.debugSection('Grab', String(val)) return val } /** * {{> grabCssPropertyFrom }} */ async grabCssPropertyFrom(locator, cssProperty) { const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty) assertElementExists(cssValues, locator) if (cssValues.length > 1) { this.debugSection('GrabCSS', `Using first element out of ${cssValues.length}`) } return cssValues[0] } /** * {{> grabAttributeFromAll }} */ async grabAttributeFromAll(locator, attr) { const res = await this._locate(locator, true) const val = await forEachAsync(res, async el => el.getAttribute(attr)) this.debugSection('GrabAttribute', String(val)) return val } /** * {{> grabAttributeFrom }} */ async grabAttributeFrom(locator, attr) { const attrs = await this.grabAttributeFromAll(locator, attr) assertElementExists(attrs, locator) if (attrs.length > 1) { this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`) } return attrs[0] } /** * {{> seeInTitle }} */ async seeInTitle(text) { const title = await this.browser.getTitle() return stringIncludes('web page title').assert(text, title) } /** * {{> seeTitleEquals }} */ 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) } /** * {{> seeTextEquals }} */ 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) { const _value = typeof value === 'boolean' ? value : value.toString() return proceedSeeField.call(this, 'assert', field, _value) } /** * {{> dontSeeInField }} * */ async dontSeeInField(field, value) { const _value = typeof value === 'boolean' ? value : value.toString() return proceedSeeField.call(this, 'negate', field, _value) } /** * Appium: not tested * {{> seeCheckboxIsChecked }} */ async seeCheckboxIsChecked(field) { return proceedSeeCheckbox.call(this, 'assert', field) } /** * Appium: not tested * {{> dontSeeCheckboxIsChecked }} */ 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()) try { return truth(`elements of ${new Locator(locator)}`, 'to be seen').assert(selected) } catch (e) { dontSeeElementError(locator) } } /** * {{> dontSeeElement }} * {{ react }} */ async dontSeeElement(locator) { const res = await this._locate(locator, false) if (!res || res.length === 0) { return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false) } const selected = await forEachAsync(res, async el => el.isDisplayed()) try { return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(selected) } catch (e) { seeElementError(locator) } } /** * {{> seeElementInDOM }} * */ async seeElementInDOM(locator) { const res = await this._res(locator) try { return empty('elements').negate(res) } catch (e) { dontSeeElementInDOMError(locator) } } /** * {{> dontSeeElementInDOM }} * */ async dontSeeElementInDOM(locator) { const res = await this._res(locator) try { return empty('elements').assert(res) } catch (e) { seeElementInDOMError(locator) } } /** * {{> 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() } /** * {{> grabBrowserLogs }} */ async grabBrowserLogs() { return browserLogs } /** * {{> 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 (${new Locator(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 (${new Locator(locator)}) is ${num}, but found ${res}`) } /** * {{> seeCssPropertiesOnElements }} */ async seeCssPropertiesOnElements(locator, cssProperties) { const res = await this._locate(locator) assertElementExists(res, locator) const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties) const elemAmount = res.length let props = [] for (const element of res) { for (const prop of Object.keys(cssProperties)) { const cssProp = await this.grabCssPropertyFrom(locator, prop) if (isColorProperty(prop)) { props.push(convertColorToRGBA(cssProp)) } else { props.push(cssProp) } } } 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 equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount) } /** * {{> 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) { const _actual = Number.isNaN(val[i]) || typeof values[i] === 'string' ? val[i] : Number.parseInt(val[i], 10) const _expected = Number.isNaN(values[i]) || typeof values[i] === 'string' ? values[i] : Number.parseInt(values[i], 10) // the attribute could be a boolean if (typeof _actual === 'boolean') return _actual === _expected if (_actual !== _expected) return false } return true }) return assert.ok(chunked.length === elemAmount, `expected all elements (${new Locator(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(