UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

1,617 lines (1,462 loc) 107 kB
let webdriverio import fs from 'fs' import assert from 'assert' import path from 'path' import crypto from 'crypto' import Helper from '@codeceptjs/helper' import promiseRetry from 'promise-retry' import { includes as stringIncludes } from '../assert/include.js' import { urlEquals, equals } from '../assert/equal.js' import store from '../store.js' import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js' import output from '../output.js' const { debug } = output import { empty } from '../assert/empty.js' import { truth } from '../assert/truth.js' import { xpathLocator, fileExists, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, modifierKeys, normalizePath, resolveUrl, getMimeType, base64EncodeFile, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' import MultipleElementsFound from './errors/MultipleElementsFound.js' import ConnectionRefused from './errors/ConnectionRefused.js' import Locator from '../locator.js' import { highlightElement } from './scripts/highlightElement.js' import { focusElement } from './scripts/focusElement.js' import { blurElement } from './scripts/blurElement.js' import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js' import { dropFile } from './scripts/dropFile.js' import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js' import WebElement from '../element/WebElement.js' import { selectElement } from './extras/elementSelection.js' import { fillRichEditor } from './extras/richTextEditor.js' const SHADOW = 'shadow' const webRoot = 'body' let browserLogs = [] /** * Wraps error objects that don't have a proper message property * This is needed for ESM compatibility with WebdriverIO error handling */ function wrapError(e) { if (e && typeof e === 'object' && !e.message) { const err = new Error(e.error || e.timeoutMsg || String(e)) err.stack = e.stack return err } return e } /** * ## 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. * * No Selenium Server, ChromeDriver, or GeckoDriver to install or start. Since WebdriverIO 9, driver management is fully automatic — WebdriverIO downloads and starts the matching driver for you. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/). Please check [Testing with WebDriver](https://codecept.io/webdriver/#testing-with-webdriver) for more details. * * 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 will be loaded dynamically in _init method // 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, strict: 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: true 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 { // In ESM, webdriverio will be checked via dynamic import in _init // The import will fail at module load time if webdriverio is missing return null } catch (e) { return ['webdriverio@^6.12.1'] } } async _init() { // Load webdriverio dynamically if (!webdriverio) { try { webdriverio = await import('webdriverio') webdriverio = webdriverio.default || webdriverio } catch (e) { throw new Error('webdriverio could not be loaded. Please install webdriverio.') } } } 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', () => {}) // Check for Bidi, because "sessionSubscribe" is an exclusive Bidi protocol feature. Otherwise, error will be thrown. if (this.browser.capabilities && this.browser.capabilities.webSocketUrl) { 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() { if (!webdriverio) await this._init() 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 (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 ARIA roles if (locator.role) { return this._locateByRole(locator) } // Handle role locators passed as Locator instances const matchedLocator = new Locator(locator) if (matchedLocator.isRole()) { return this._locateByRole(matchedLocator.locator) } 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) } /** * Locate elements by ARIA role using WebdriverIO accessibility selectors * * @param {object} locator - role locator object { role: string, text?: string, exact?: boolean } */ async _locateByRole(locator) { const role = locator.role if (!locator.text) { return this.browser.$$(`[role="${role}"]`) } const elements = await this.browser.$$(`[role="${role}"]`) const filteredElements = [] const matchFn = locator.exact === true ? t => t === locator.text : t => t && t.includes(locator.text) for (const element of elements) { const texts = await getElementTextAttributes.call(this, element) if (texts.some(matchFn)) { filteredElements.push(element) } } return filteredElements } /** * {{> grabWebElements }} * */ async grabWebElements(locator) { const elements = await this._locate(locator) return elements.map(element => new WebElement(element, this)) } /** * {{> grabWebElement }} * */ async grabWebElement(locator) { const elements = await this._locate(locator) if (elements.length === 0) { throw new ElementNotFound(locator, 'Element') } return new WebElement(elements[0], this) } /** * 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 }} * */ async click(locator, context = null) { const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? '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 = selectElement(res, locator, this) highlightActiveElement.call(this, elem) return this.browser[clickMethod](getElementId(elem)) } /** * {{> forceClick }} * */ 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 = selectElement(res, locator, this) 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 }} * */ 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 = selectElement(res, locator, this) highlightActiveElement.call(this, elem) return elem.doubleClick() } /** * {{> rightClick }} * */ 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 = selectElement(res, locator, this) await el.moveTo() if (this.browser.isW3C) { return el.click({ button: 'right' }) } // JSON Wire version await this.browser.buttonDown(2) } /** * Performs click at specific coordinates. * If locator is provided, the coordinates are relative to the element's top-left corner. * If locator is not provided, the coordinates are relative to the body element. * * ```js * // Click at coordinates (100, 200) relative to body * I.clickXY(100, 200); * * // Click at coordinates (50, 30) relative to element's top-left corner * I.clickXY('#someElement', 50, 30); * ``` * * @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element. * @param {number} [x] X coordinate relative to element's top-left, or Y coordinate if locator is a number. * @param {number} [y] Y coordinate relative to element's top-left. * @returns {Promise<void>} */ async clickXY(locator, x, y) { // If locator is a number, treat it as X coordinate and use body as base if (typeof locator === 'number') { const globalX = locator const globalY = x locator = '//body' x = globalX y = globalY } // Locate the base element const res = await this._locate(withStrictLocator(locator), true) assertElementExists(res, locator, 'Element to click') const el = usingFirstElement(res) // Get element position and size to calculate top-left corner const location = await el.getLocation() const size = await el.getSize() // WebDriver clicks at center by default, so we need to offset from center to top-left // then add our desired x, y coordinates const offsetX = -(size.width / 2) + x const offsetY = -(size.height / 2) + y if (this.browser.isW3C) { // Use performActions for W3C WebDriver return this.browser.performActions([ { type: 'pointer', id: 'pointer1', parameters: { pointerType: 'mouse' }, actions: [ { type: 'pointerMove', origin: el, duration: 0, x: Math.round(offsetX), y: Math.round(offsetY), }, { type: 'pointerDown', button: 0 }, { type: 'pointerUp', button: 0 }, ], }, ]) } // Fallback for non-W3C browsers await el.moveTo({ xOffset: Math.round(offsetX), yOffset: Math.round(offsetY) }) return el.click() } /** * {{> forceRightClick }} * */ 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 }} * {{ custom }} * */ async fillField(field, value, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') const elem = selectElement(res, field, this) highlightActiveElement.call(this, elem) if (this.isWeb !== false && await fillRichEditor(this, elem, value)) { return } try { await elem.clearValue() } catch (err) { if (err.message && err.message.includes('invalid element state')) { await this.executeScript(el => { el.value = '' }, elem) } else { throw err } } await elem.setValue(value.toString()) } /** * {{> appendField }} */ async appendField(field, value, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') const elem = selectElement(res, field, this) highlightActiveElement.call(this, elem) return elem.addValue(value.toString()) } /** * {{> clearField }} * */ async clearField(field, context = null) { const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') const elem = selectElement(res, field, this) highlightActiveElement.call(this, elem) return elem.clearValue(getElementId(elem)) } /** * {{> selectOption }} */ async selectOption(select, option, context = null) { const locateFn = prepareLocateFn.call(this, context) const matchedLocator = new Locator(select) // Strict locator if (!matchedLocator.isFuzzy()) { this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`) const els = await locateFn(select) assertElementExists(els, select, 'Selectable element') return proceedSelectOption.call(this, selectElement(els, select, this), option) } // Fuzzy: try combobox this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`) let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value }) if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option) // Fuzzy: try listbox els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value }) if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option) // Fuzzy: try native select const res = await findFields.call(this, select, context) assertElementExists(res, select, 'Selectable field') return proceedSelectOption.call(this, selectElement(res, select, this), option) } /** * Appium: not tested * * {{> attachFile }} */ async attachFile(locator, pathToFile, context = null) { let file = path.join(store.codeceptDir, pathToFile) if (!fileExists(file)) { throw new Error(`File at ${file} can not be found on local system`) } const res = await findFields.call(this, locator, context) this.debug(`Uploading ${file}`) if (res.length) { const el = selectElement(res, locator, this) const tag = await this.browser.execute(function (elem) { return elem.tagName }, el) const type = await this.browser.execute(function (elem) { return elem.type }, el) if (tag === 'INPUT' && type === 'file') { 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) } } const targetRes = res.length ? res : await this._locate(locator) assertElementExists(targetRes, locator, 'Element') const targetEl = selectElement(targetRes, locator, this) const fileData = { base64Content: base64EncodeFile(file), fileName: path.basename(file), mimeType: getMimeType(path.basename(file)), } return this.browser.execute(dropFile, targetEl, fileData) } /** * Appium: not tested * {{> checkOption }} */ async checkOption(field, context = null) { const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' const locateFn = prepareLocateFn.call(this, context) const res = await findCheckable.call(this, field, locateFn) assertElementExists(res, field, 'Checkable') const elem = selectElement(res, field, this) const elementId = getElementId(elem) highlightActiveElement.call(this, elem) const isSelected = await isElementChecked(this.browser, 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.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick' const locateFn = prepareLocateFn.call(this, context) const res = await findCheckable.call(this, field, locateFn) assertElementExists(res, field, 'Checkable') const elem = selectElement(res, field, this) const elementId = getElementId(elem) highlightActiveElement.call(this, elem) const isSelected = await isElementChecked(this.browser, 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 }} * */ 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 }} * */ async dontSee(text, context = null) { return proceedSee.call(this, 'negate', text, context) } /** * {{> seeInField }} * */ async seeInField(field, value, context = null) { const _value = typeof value === 'boolean' ? value : value.toString() return proceedSeeField.call(this, 'assert', field, _value, context) } /** * {{> dontSeeInField }} * */ async dontSeeInField(field, value, con