codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
1,642 lines (1,484 loc) • 91.6 kB
JavaScript
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(