codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
1,754 lines (1,554 loc) • 52.9 kB
JavaScript
let EC
let Key
let Button
let ProtractorBy
let ProtractorExpectedConditions
const path = require('path')
const Helper = require('@codeceptjs/helper')
const stringIncludes = require('../assert/include').includes
const { urlEquals, equals } = require('../assert/equal')
const { empty } = require('../assert/empty')
const { truth } = require('../assert/truth')
const { xpathLocator, fileExists, convertCssPropertiesToCamelCase, screenshotOutputFolder } = require('../utils')
const { isColorProperty, convertColorToRGBA } = require('../colorUtils')
const ElementNotFound = require('./errors/ElementNotFound')
const ConnectionRefused = require('./errors/ConnectionRefused')
const Locator = require('../locator')
let withinStore = {}
let Runner
/**
* Protractor helper is based on [Protractor library](http://www.protractortest.org) and used for testing web applications.
*
* Protractor requires [Selenium Server and ChromeDriver/GeckoDriver to be installed](http://codecept.io/quickstart/#prepare-selenium-server).
* To test non-Angular applications please make sure you have `angular: false` in configuration file.
*
* ### Configuration
*
* This helper should be configured in codecept.conf.ts or codecept.conf.js
*
* * `url` - base url of website to be tested
* * `browser` - browser in which perform testing
* * `angular` (optional, default: true): disable this option to run tests for non-Angular applications.
* * `driver` - which protractor driver to use (local, direct, session, hosted, sauce, browserstack). By default set to 'hosted' which requires selenium server to be started.
* * `restart` (optional, default: true) - restart browser between tests.
* * `smartWait`: (optional) **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000
* * `disableScreenshots` (optional, default: false) - don't save screenshot on failure
* * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
* * `uniqueScreenshotNames` (optional, default: false) - option to prevent screenshot override if you have scenarios with the same name in different suites
* * `keepBrowserState` (optional, default: false) - keep browser state between tests when `restart` set to false.
* * `seleniumAddress` - Selenium address to connect (default: http://localhost:4444/wd/hub)
* * `rootElement` - Root element of AngularJS application (default: body)
* * `getPageTimeout` (optional) sets default timeout for a page to be loaded. 10000 by default.
* * `waitForTimeout`: (optional) sets default wait time in _ms_ for all `wait*` functions. 1000 by default.
* * `scriptsTimeout`: (optional) timeout in milliseconds for each script run on the browser, 10000 by default.
* * `windowSize`: (optional) default window size. Set to `maximize` or a dimension in the format `640x480`.
* * `manualStart` (optional, default: false) - do not start browser before a test, start it manually inside a helper with `this.helpers.WebDriver._startBrowser()`
* * `capabilities`: {} - list of [Desired Capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities)
* * `proxy`: set proxy settings
*
* other options are the same as in [Protractor config](https://github.com/angular/protractor/blob/master/docs/referenceConf.js).
*
* #### Sample Config
*
* ```json
* {
* "helpers": {
* "Protractor" : {
* "url": "http://localhost",
* "browser": "chrome",
* "smartWait": 5000,
* "restart": false
* }
* }
* }
* ```
*
* #### Config for Non-Angular application:
*
* ```json
* {
* "helpers": {
* "Protractor" : {
* "url": "http://localhost",
* "browser": "chrome",
* "angular": false
* }
* }
* }
* ```
*
* #### Config for Headless Chrome
*
* ```json
* {
* "helpers": {
* "Protractor" : {
* "url": "http://localhost",
* "browser": "chrome",
* "capabilities": {
* "chromeOptions": {
* "args": [ "--headless", "--disable-gpu", "--no-sandbox" ]
* }
* }
* }
* }
* }
* ```
*
* ## Access From Helpers
*
* Receive a WebDriverIO client from a custom helper by accessing `browser` property:
*
* ```js
* this.helpers['Protractor'].browser
* ```
*
* ## Methods
*/
class Protractor extends Helper {
constructor(config) {
// process.env.SELENIUM_PROMISE_MANAGER = false; // eslint-disable-line
super(config)
this.isRunning = false
this._setConfig(config)
console.log('Protractor helper is deprecated as well as Protractor itself.\nThis helper will be removed in next major release')
}
_validateConfig(config) {
const defaults = {
browser: 'chrome',
url: 'http://localhost',
seleniumAddress: 'http://localhost:4444/wd/hub',
fullPageScreenshots: false,
rootElement: 'body',
allScriptsTimeout: 10000,
scriptTimeout: 10000,
waitForTimeout: 1000, // ms
windowSize: null,
getPageTimeout: 10000,
driver: 'hosted',
capabilities: {},
angular: true,
restart: true,
}
config = Object.assign(defaults, config)
if (!config.allScriptsTimeout) config.allScriptsTimeout = config.scriptsTimeout
if (!config.scriptTimeout) config.scriptTimeout = config.scriptsTimeout
if (config.proxy) config.capabilities.proxy = config.proxy
if (config.browser) config.capabilities.browserName = config.browser
config.waitForTimeoutInSeconds = config.waitForTimeout / 1000 // convert to seconds
return config
}
async _init() {
process.on('unhandledRejection', reason => {
if (reason.message.indexOf('ECONNREFUSED') > 0) {
this.browser = null
}
})
Runner = require('protractor/built/runner').Runner
ProtractorBy = require('protractor').ProtractorBy
Key = require('protractor').Key
Button = require('protractor').Button
ProtractorExpectedConditions = require('protractor').ProtractorExpectedConditions
return Promise.resolve()
}
static _checkRequirements() {
try {
require('protractor')
require('assert').ok(require('protractor/built/runner').Runner)
} catch (e) {
return ['protractor@^5.3.0']
}
}
static _config() {
return [
{ name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
{
name: 'driver',
message: 'Protractor driver (local, direct, session, hosted, sauce, browserstack)',
default: 'hosted',
},
{ name: 'browser', message: 'Browser in which testing will be performed', default: 'chrome' },
{ name: 'rootElement', message: 'Root element of AngularJS application', default: 'body' },
{
name: 'angular',
message: 'Enable AngularJS synchronization',
default: false,
type: 'confirm',
},
]
}
async _beforeStep() {
if (!this.insideAngular) {
return this.amOutsideAngularApp()
}
}
async _beforeSuite() {
if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
this.debugSection('Session', 'Starting singleton browser session')
return this._startBrowser()
}
}
async _startBrowser() {
try {
const runner = new Runner(this.options)
this.browser = runner.createBrowser()
await this.browser.ready
} catch (err) {
if (err.toString().indexOf('ECONNREFUSED')) {
throw new ConnectionRefused(err)
}
throw err
}
if (this.options.angular) {
await this.amInsideAngularApp()
} else {
await this.amOutsideAngularApp()
}
loadGlobals(this.browser)
if (this.options.windowSize === 'maximize') {
await this.resizeWindow(this.options.windowSize)
} else if (this.options.windowSize) {
const size = this.options.windowSize.split('x')
await this.resizeWindow(parseInt(size[0], 10), parseInt(size[1], 10))
}
this.context = this.options.rootElement
this.isRunning = true
return this.browser.ready
}
async _before() {
if (this.options.restart && !this.options.manualStart) return this._startBrowser()
if (!this.isRunning && !this.options.manualStart) return this._startBrowser()
}
async _after() {
if (!this.browser) return
if (!this.isRunning) return
if (this.options.restart) {
this.isRunning = false
return this.browser.quit()
}
if (this.options.keepBrowserState) return
const dialog = await this.grabPopupText()
if (dialog) {
await this.cancelPopup()
}
if (!this.options.keepCookies) {
await this.browser.manage().deleteAllCookies()
}
let url
try {
url = await this.browser.getCurrentUrl()
} catch (err) {
// Ignore, as above will throw if no webpage has been loaded
}
if (url && !/data:,/i.test(url)) {
await this.browser.executeScript('localStorage.clear();')
}
return this.closeOtherTabs()
}
async _failed() {
await this._withinEnd()
}
async _finishTest() {
if (!this.options.restart && this.isRunning) return this.browser.quit()
}
async _withinBegin(locator) {
withinStore.elFn = this.browser.findElement
withinStore.elsFn = this.browser.findElements
const frame = isFrameLocator(locator)
if (frame) {
if (Array.isArray(frame)) {
withinStore.frame = frame.join('>')
return this.switchTo(null).then(() => frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()))
}
withinStore.frame = frame
return this.switchTo(locator)
}
this.context = locator
const context = await global.element(guessLocator(locator) || global.by.css(locator))
if (!context) throw new ElementNotFound(locator)
this.browser.findElement = l => (l ? context.element(l).getWebElement() : context.getWebElement())
this.browser.findElements = l => context.all(l).getWebElements()
return context
}
async _withinEnd() {
if (!isWithin()) return
if (withinStore.frame) {
withinStore = {}
return this.switchTo(null)
}
this.browser.findElement = withinStore.elFn
this.browser.findElements = withinStore.elsFn
withinStore = {}
this.context = this.options.rootElement
}
_session() {
const defaultSession = this.browser
return {
start: async opts => {
opts = this._validateConfig(Object.assign(this.options, opts))
this.debugSection('New Browser', JSON.stringify(opts))
const runner = new Runner(opts)
const res = await this.browser.executeScript('return [window.outerWidth, window.outerHeight]')
const browser = runner.createBrowser(null, this.browser)
await browser.ready
await browser.waitForAngularEnabled(this.insideAngular)
await browser.manage().window().setSize(parseInt(res[0], 10), parseInt(res[1], 10))
return browser.ready
},
stop: async browser => {
return browser.close()
},
loadVars: async browser => {
if (isWithin()) throw new Error("Can't start session inside within block")
this.browser = browser
loadGlobals(this.browser)
},
restoreVars: async () => {
if (isWithin()) await this._withinEnd()
this.browser = defaultSession
loadGlobals(this.browser)
},
}
}
/**
* Use [Protractor](https://www.protractortest.org/#/api) 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://www.protractortest.org/#/api?view=ProtractorBrowser)) } object from Protractor API is available.
*
* ```js
* I.useProtractorTo('change url via in-page navigation', async ({ browser }) {
* await browser.setLocation('api');
* });
* ```
*
* @param {string} description used to show in logs.
* @param {function} fn async functuion that executed with Protractor helper as argument
*/
useProtractorTo(description, fn) {
return this._useTo(...arguments)
}
/**
* Switch to non-Angular mode,
* start using WebDriver instead of Protractor in this session
*/
async amOutsideAngularApp() {
if (!this.browser) return
await this.browser.waitForAngularEnabled(false)
return Promise.resolve((this.insideAngular = false))
}
/**
* Enters Angular mode (switched on by default)
* Should be used after "amOutsideAngularApp"
*/
async amInsideAngularApp() {
await this.browser.waitForAngularEnabled(true)
return Promise.resolve((this.insideAngular = true))
}
/**
* Get elements by different locator types, including strict locator
* Should be used in custom helpers:
*
* ```js
* this.helpers['Protractor']._locate({name: 'password'}).then //...
* ```
* To use SmartWait and wait for element to appear on a page, add `true` as second arg:
*
* ```js
* this.helpers['Protractor']._locate({name: 'password'}, true).then //...
* ```
*
*/
async _locate(locator, smartWait = false) {
return this._smartWait(() => this.browser.findElements(guessLocator(locator) || global.by.css(locator)), smartWait)
}
async _smartWait(fn, enabled = true) {
if (!this.options.smartWait || !enabled) return fn()
await this.browser.manage().timeouts().implicitlyWait(this.options.smartWait)
const res = await fn()
await this.browser.manage().timeouts().implicitlyWait(0)
return res
}
/**
* Find a checkbox by providing human readable text:
*
* ```js
* this.helpers['Protractor']._locateCheckable('I agree with terms and conditions').then // ...
* ```
*/
async _locateCheckable(locator) {
return findCheckable.call(this, this.browser, locator)
}
/**
* Find a clickable element by providing human readable text:
*
* ```js
* this.helpers['Protractor']._locateClickable('Next page').then // ...
* ```
*/
async _locateClickable(locator) {
return findClickable.call(this, this.browser, locator)
}
/**
* Find field elements by providing human readable text:
*
* ```js
* this.helpers['Protractor']._locateFields('Your email').then // ...
* ```
*/
async _locateFields(locator) {
return findFields.call(this, this.browser, locator)
}
/**
* {{> amOnPage }}
*/
async amOnPage(url) {
if (!/^\w+\:\/\//.test(url)) {
url = this.options.url + url
}
const res = await this.browser.get(url)
this.debug(`Visited ${url}`)
return res
}
/**
* {{> click }}
*/
async click(locator, context = null) {
let matcher = this.browser
if (context) {
const els = await this._locate(context, true)
assertElementExists(els, context)
matcher = els[0]
}
const el = await findClickable.call(this, matcher, locator)
return el.click()
}
/**
* {{> doubleClick }}
*/
async doubleClick(locator, context = null) {
let matcher = this.browser
if (context) {
const els = await this._locate(context, true)
assertElementExists(els, context)
matcher = els[0]
}
const el = await findClickable.call(this, matcher, locator)
return this.browser.actions().doubleClick(el).perform()
}
/**
* {{> rightClick }}
*/
async rightClick(locator, context = null) {
/**
* just press button if no selector is given
*/
if (locator === undefined) {
return this.browser.actions().click(Button.RIGHT).perform()
}
let matcher = this.browser
if (context) {
const els = await this._locate(context, true)
assertElementExists(els, context)
matcher = els[0]
}
const el = await findClickable.call(this, matcher, locator)
await this.browser.actions().mouseMove(el).perform()
return this.browser.actions().click(Button.RIGHT).perform()
}
/**
* {{> moveCursorTo }}
*/
async moveCursorTo(locator, offsetX = null, offsetY = null) {
let offset = null
if (offsetX !== null || offsetY !== null) {
offset = { x: offsetX, y: offsetY }
}
const els = await this._locate(locator, true)
assertElementExists(els, locator)
return this.browser.actions().mouseMove(els[0], offset).perform()
}
/**
* {{> 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 }}
*/
dontSee(text, context = null) {
return proceedSee.call(this, 'negate', text, context)
}
/**
* {{> grabBrowserLogs }}
*/
async grabBrowserLogs() {
return this.browser.manage().logs().get('browser')
}
/**
* {{> grabCurrentUrl }}
*/
async grabCurrentUrl() {
return this.browser.getCurrentUrl()
}
/**
* {{> selectOption }}
*/
async selectOption(select, option) {
const fields = await findFields(this.browser, select)
assertElementExists(fields, select, 'Selectable field')
if (!Array.isArray(option)) {
option = [option]
}
const field = fields[0]
const promises = []
for (const key in option) {
const opt = xpathLocator.literal(option[key])
let els = await field.findElements(global.by.xpath(Locator.select.byVisibleText(opt)))
if (!els.length) {
els = await field.findElements(global.by.xpath(Locator.select.byValue(opt)))
}
els.forEach(el => promises.push(el.click()))
}
return Promise.all(promises)
}
/**
* {{> fillField }}
*/
async fillField(field, value) {
const els = await findFields(this.browser, field)
await els[0].clear()
return els[0].sendKeys(value.toString())
}
/**
* {{> pressKey }}
* {{ keys }}
*/
async pressKey(key) {
let modifier
if (Array.isArray(key) && ~['Control', 'Command', 'Shift', 'Alt'].indexOf(key[0])) {
modifier = Key[key[0].toUpperCase()]
key = key[1]
}
// guess special key in Selenium Webdriver list
if (Key[key.toUpperCase()]) {
key = Key[key.toUpperCase()]
}
const action = this.browser.actions()
if (modifier) action.keyDown(modifier)
action.sendKeys(key)
if (modifier) action.keyUp(modifier)
return action.perform()
}
/**
* {{> attachFile }}
*/
async attachFile(locator, pathToFile) {
const file = path.join(global.codecept_dir, pathToFile)
if (!fileExists(file)) {
throw new Error(`File at ${file} can not be found on local system`)
}
const els = await findFields(this.browser, locator)
assertElementExists(els, locator, 'Field')
if (this.options.browser !== 'phantomjs') {
const remote = require('selenium-webdriver/remote')
this.browser.setFileDetector(new remote.FileDetector())
}
return els[0].sendKeys(file)
}
/**
* {{> seeInField }}
*/
async seeInField(field, value) {
const _value = typeof value === 'boolean' ? value : value.toString()
return proceedSeeInField.call(this, 'assert', field, _value)
}
/**
* {{> dontSeeInField }}
*/
async dontSeeInField(field, value) {
const _value = typeof value === 'boolean' ? value : value.toString()
return proceedSeeInField.call(this, 'negate', field, _value)
}
/**
* {{> appendField }}
*/
async appendField(field, value) {
const els = await findFields(this.browser, field)
assertElementExists(els, field, 'Field')
return els[0].sendKeys(value.toString())
}
/**
* {{> clearField }}
*/
async clearField(field) {
const els = await findFields(this.browser, field)
assertElementExists(els, field, 'Field')
return els[0].clear()
}
/**
* {{> checkOption }}
*/
async checkOption(field, context = null) {
let matcher = this.browser
if (context) {
const els = await this._locate(context, true)
assertElementExists(els, context)
matcher = els[0]
}
const els = await findCheckable(matcher, field)
assertElementExists(els, field, 'Checkbox or radio')
const isSelected = await els[0].isSelected()
if (!isSelected) return els[0].click()
}
/**
* {{> uncheckOption }}
*/
async uncheckOption(field, context = null) {
let matcher = this.browser
if (context) {
const els = await this._locate(context, true)
assertElementExists(els, context)
matcher = els[0]
}
const els = await findCheckable(matcher, field)
assertElementExists(els, field, 'Checkbox or radio')
const isSelected = await els[0].isSelected()
if (isSelected) return els[0].click()
}
/**
* {{> seeCheckboxIsChecked }}
*/
async seeCheckboxIsChecked(field) {
return proceedIsChecked.call(this, 'assert', field)
}
/**
* {{> dontSeeCheckboxIsChecked }}
*/
async dontSeeCheckboxIsChecked(field) {
return proceedIsChecked.call(this, 'negate', field)
}
/**
* {{> grabTextFromAll }}
*/
async grabTextFromAll(locator) {
const els = await this._locate(locator)
const texts = []
for (const el of els) {
texts.push(await el.getText())
}
return texts
}
/**
* {{> 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 els = await this._locate(locator)
const html = await Promise.all(
els.map(el => {
return this.browser.executeScript('return arguments[0].innerHTML;', el)
}),
)
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 els = await findFields(this.browser, locator)
const values = await Promise.all(els.map(el => el.getAttribute('value')))
return values
}
/**
* {{> grabValueFrom }}
*/
async grabValueFrom(locator) {
const values = await this.grabValueFromAll(locator)
assertElementExists(values, locator, 'Field')
if (values.length > 1) {
this.debugSection('GrabValue', `Using first element out of ${values.length}`)
}
return values[0]
}
/**
* {{> grabCssPropertyFromAll }}
*/
async grabCssPropertyFromAll(locator, cssProperty) {
const els = await this._locate(locator, true)
const values = await Promise.all(els.map(el => el.getCssValue(cssProperty)))
return values
}
/**
* {{> 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 els = await this._locate(locator)
const array = []
for (let index = 0; index < els.length; index++) {
const el = els[index]
array.push(await el.getAttribute(attr))
}
return array
}
/**
* {{> 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) {
return this.browser.getTitle().then(title => stringIncludes('web page title').assert(text, title))
}
/**
* {{> seeTitleEquals }}
*/
async seeTitleEquals(text) {
const title = await this.browser.getTitle()
return equals('web page title').assert(title, text)
}
/**
* {{> dontSeeInTitle }}
*/
async dontSeeInTitle(text) {
return this.browser.getTitle().then(title => stringIncludes('web page title').negate(text, title))
}
/**
* {{> grabTitle }}
*/
async grabTitle() {
return this.browser.getTitle().then(title => {
this.debugSection('Title', title)
return title
})
}
/**
* {{> seeElement }}
*/
async seeElement(locator) {
let els = await this._locate(locator, true)
els = await Promise.all(els.map(el => el.isDisplayed()))
return empty('elements').negate(els.filter(v => v).fill('ELEMENT'))
}
/**
* {{> dontSeeElement }}
*/
async dontSeeElement(locator) {
let els = await this._locate(locator, false)
els = await Promise.all(els.map(el => el.isDisplayed()))
return empty('elements').assert(els.filter(v => v).fill('ELEMENT'))
}
/**
* {{> seeElementInDOM }}
*/
async seeElementInDOM(locator) {
return this.browser.findElements(guessLocator(locator) || global.by.css(locator)).then(els => empty('elements').negate(els.fill('ELEMENT')))
}
/**
* {{> dontSeeElementInDOM }}
*/
async dontSeeElementInDOM(locator) {
return this.browser.findElements(guessLocator(locator) || global.by.css(locator)).then(els => empty('elements').assert(els.fill('ELEMENT')))
}
/**
* {{> seeInSource }}
*/
async seeInSource(text) {
return this.browser.getPageSource().then(source => stringIncludes('HTML source of a page').assert(text, source))
}
/**
* {{> grabSource }}
*/
async grabSource() {
return this.browser.getPageSource()
}
/**
* {{> dontSeeInSource }}
*/
async dontSeeInSource(text) {
return this.browser.getPageSource().then(source => stringIncludes('HTML source of a page').negate(text, source))
}
/**
* {{> seeNumberOfElements }}
*/
async seeNumberOfElements(locator, num) {
const elements = await this._locate(locator)
return equals(`expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`).assert(elements.length, num)
}
/**
* {{> seeNumberOfVisibleElements }}
*/
async seeNumberOfVisibleElements(locator, num) {
const res = await this.grabNumberOfVisibleElements(locator)
return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert(res, num)
}
/**
* {{> grabNumberOfVisibleElements }}
*/
async grabNumberOfVisibleElements(locator) {
let els = await this._locate(locator)
els = await Promise.all(els.map(el => el.isDisplayed()))
return els.length
}
/**
* {{> seeCssPropertiesOnElements }}
*/
async seeCssPropertiesOnElements(locator, cssProperties) {
const els = await this._locate(locator)
assertElementExists(els, locator)
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties)
const attributeNames = Object.keys(cssPropertiesCamelCase)
const expectedValues = attributeNames.map(name => cssPropertiesCamelCase[name])
const missingAttributes = []
for (const el of els) {
const attributeValues = await Promise.all(attributeNames.map(attr => el.getCssValue(attr)))
const missing = attributeValues.filter((actual, i) => {
const prop = attributeNames[i]
let propValue = actual
if (isColorProperty(prop) && propValue) {
propValue = convertColorToRGBA(propValue)
}
return propValue !== expectedValues[i]
})
if (missing.length) {
missingAttributes.push(...missing)
}
}
return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(missingAttributes.length, 0)
}
/**
* {{> seeAttributesOnElements }}
*/
async seeAttributesOnElements(locator, attributes) {
const els = await this._locate(locator)
assertElementExists(els, locator)
const attributeNames = Object.keys(attributes)
const expectedValues = attributeNames.map(name => attributes[name])
const missingAttributes = []
for (const el of els) {
const attributeValues = await Promise.all(attributeNames.map(attr => el.getAttribute(attr)))
const missing = attributeValues.filter((actual, i) => {
if (expectedValues[i] instanceof RegExp) {
return expectedValues[i].test(actual)
}
return actual !== expectedValues[i]
})
if (missing.length) {
missingAttributes.push(...missing)
}
}
return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(missingAttributes.length, 0)
}
/**
* {{> executeScript }}
*/
async executeScript() {
return this.browser.executeScript.apply(this.browser, arguments)
}
/**
* {{> executeAsyncScript }}
*/
async executeAsyncScript() {
this.browser.manage().timeouts().setScriptTimeout(this.options.scriptTimeout)
return this.browser.executeAsyncScript.apply(this.browser, arguments)
}
/**
* {{> seeInCurrentUrl }}
*/
async seeInCurrentUrl(url) {
return this.browser.getCurrentUrl().then(currentUrl => stringIncludes('url').assert(url, currentUrl))
}
/**
* {{> dontSeeInCurrentUrl }}
*/
async dontSeeInCurrentUrl(url) {
return this.browser.getCurrentUrl().then(currentUrl => stringIncludes('url').negate(url, currentUrl))
}
/**
* {{> seeCurrentUrlEquals }}
*/
async seeCurrentUrlEquals(url) {
return this.browser.getCurrentUrl().then(currentUrl => urlEquals(this.options.url).assert(url, currentUrl))
}
/**
* {{> dontSeeCurrentUrlEquals }}
*/
async dontSeeCurrentUrlEquals(url) {
return this.browser.getCurrentUrl().then(currentUrl => urlEquals(this.options.url).negate(url, currentUrl))
}
/**
* {{> saveElementScreenshot }}
*
*/
async saveElementScreenshot(locator, fileName) {
const outputFile = screenshotOutputFolder(fileName)
const writeFile = (png, outputFile) => {
const fs = require('fs')
const stream = fs.createWriteStream(outputFile)
stream.write(Buffer.from(png, 'base64'))
stream.end()
return new Promise(resolve => stream.on('finish', resolve))
}
const res = await this._locate(locator)
assertElementExists(res, locator)
if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`)
const elem = res[0]
this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
const png = await elem.takeScreenshot()
return writeFile(png, outputFile)
}
/**
* {{> saveScreenshot }}
*/
async saveScreenshot(fileName, fullPage = false) {
const outputFile = screenshotOutputFolder(fileName)
const writeFile = (png, outputFile) => {
const fs = require('fs')
const stream = fs.createWriteStream(outputFile)
stream.write(Buffer.from(png, 'base64'))
stream.end()
return new Promise(resolve => stream.on('finish', resolve))
}
if (!fullPage) {
this.debug(`Screenshot has been saved to ${outputFile}`)
const png = await this.browser.takeScreenshot()
return writeFile(png, outputFile)
}
let { width, height } = await this.browser.executeScript(() => ({
height: document.body.scrollHeight,
width: document.body.scrollWidth,
}))
if (height < 100) height = 500
await this.browser.manage().window().setSize(width, height)
this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`)
const png = await this.browser.takeScreenshot()
return writeFile(png, outputFile)
}
/**
* {{> clearCookie }}
*/
async clearCookie(cookie = null) {
if (!cookie) {
return this.browser.manage().deleteAllCookies()
}
return this.browser.manage().deleteCookie(cookie)
}
/**
* {{> seeCookie }}
*/
async seeCookie(name) {
return this.browser
.manage()
.getCookie(name)
.then(res => truth(`cookie ${name}`, 'to be set').assert(res))
}
/**
* {{> dontSeeCookie }}
*/
async dontSeeCookie(name) {
return this.browser
.manage()
.getCookie(name)
.then(res => truth(`cookie ${name}`, 'to be set').negate(res))
}
/**
* {{> grabCookie }}
*
* Returns cookie in JSON [format](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object).
*/
async grabCookie(name) {
if (!name) return this.browser.manage().getCookies()
return this.browser.manage().getCookie(name)
}
/**
* Accepts the active JavaScript native popup window, as created by window.alert|window.confirm|window.prompt.
* Don't confuse popups with modal windows, as created by [various
* libraries](http://jster.net/category/windows-modals-popups). Appium: support only web testing
*/
async acceptPopup() {
return this.browser.switchTo().alert().accept()
}
/**
* Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
*/
async cancelPopup() {
return this.browser.switchTo().alert().dismiss()
}
/**
* {{> seeInPopup }}
*/
async seeInPopup(text) {
const popupAlert = await this.browser.switchTo().alert()
const res = await popupAlert.getText()
if (res === null) {
throw new Error('Popup is not opened')
}
stringIncludes('text in popup').assert(text, res)
}
/**
* Grab the text within the popup. If no popup is visible then it will return null
*
* ```js
* await I.grabPopupText();
* ```
*/
async grabPopupText() {
try {
const dialog = await this.browser.switchTo().alert()
if (dialog) {
return dialog.getText()
}
} catch (e) {
if (e.message.match(/no.*?(alert|modal)/i)) {
// Don't throw an error
return null
}
throw e
}
}
/**
* {{> resizeWindow }}
*/
async resizeWindow(width, height) {
if (width === 'maximize') {
const res = await this.browser.executeScript('return [screen.width, screen.height]')
return this.browser.manage().window().setSize(parseInt(res[0], 10), parseInt(res[1], 10))
}
return this.browser.manage().window().setSize(parseInt(width, 10), parseInt(height, 10))
}
/**
* {{> dragAndDrop }}
*/
async dragAndDrop(srcElement, destElement) {
const srcEl = await this._locate(srcElement, true)
const destEl = await this._locate(destElement, true)
assertElementExists(srcEl, srcElement)
assertElementExists(destEl, destElement)
return this.browser.actions().dragAndDrop(srcEl[0], destEl[0]).perform()
}
/**
* Close all tabs except for the current one.
*
* ```js
* I.closeOtherTabs();
* ```
*/
async closeOtherTabs() {
const client = this.browser
const handles = await client.getAllWindowHandles()
const currentHandle = await client.getWindowHandle()
const otherHandles = handles.filter(handle => handle !== currentHandle)
if (!otherHandles || !otherHandles.length) return
let p = Promise.resolve()
otherHandles.forEach(handle => {
p = p.then(() =>
client
.switchTo()
.window(handle)
.then(() => client.close()),
)
})
p = p.then(() => client.switchTo().window(currentHandle))
return p
}
/**
* Close current tab
*
* ```js
* I.closeCurrentTab();
* ```
*/
async closeCurrentTab() {
const client = this.browser
const currentHandle = await client.getWindowHandle()
const nextHandle = await this._getWindowHandle(-1)
await client.switchTo().window(currentHandle)
await client.close()
return client.switchTo().window(nextHandle)
}
/**
* Get the window handle relative to the current handle. i.e. the next handle or the previous.
* @param {Number} offset Offset from current handle index. i.e. offset < 0 will go to the previous handle and positive number will go to the next window handle in sequence.
*/
async _getWindowHandle(offset = 0) {
const client = this.browser
const handles = await client.getAllWindowHandles()
const index = handles.indexOf(await client.getWindowHandle())
const nextIndex = index + offset
return handles[nextIndex]
// return handles[(index + offset) % handles.length];
}
/**
* Open new tab and switch to it
*
* ```js
* I.openNewTab();
* ```
*/
async openNewTab() {
const client = this.browser
await this.executeScript('window.open("about:blank")')
const handles = await client.getAllWindowHandles()
await client.switchTo().window(handles[handles.length - 1])
}
/**
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
*
* ```js
* I.switchToNextTab();
* I.switchToNextTab(2);
* ```
*/
async switchToNextTab(num = 1) {
const client = this.browser
const newHandle = await this._getWindowHandle(num)
if (!newHandle) {
throw new Error(`There is no ability to switch to next tab with offset ${num}`)
}
return client.switchTo().window(newHandle)
}
/**
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
*
* ```js
* I.switchToPreviousTab();
* I.switchToPreviousTab(2);
* ```
*/
async switchToPreviousTab(num = 1) {
const client = this.browser
const newHandle = await this._getWindowHandle(-1 * num)
if (!newHandle) {
throw new Error(`There is no ability to switch to previous tab with offset ${num}`)
}
return client.switchTo().window(newHandle)
}
/**
* {{> grabNumberOfOpenTabs }}
*/
async grabNumberOfOpenTabs() {
const pages = await this.browser.getAllWindowHandles()
return pages.length
}
/**
* {{> switchTo }}
*/
async switchTo(locator) {
if (Number.isInteger(locator)) {
return this.browser.switchTo().frame(locator)
}
if (!locator) {
return this.browser.switchTo().frame(null)
}
const els = await this._locate(withStrictLocator.call(this, locator), true)
assertElementExists(els, locator)
return this.browser.switchTo().frame(els[0])
}
/**
* {{> wait }}
*/
wait(sec) {
return this.browser.sleep(sec * 1000)
}
/**
* {{> waitForElement }}
*/
async waitForElement(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.presenceOf(el), aSec * 1000)
}
async waitUntilExists(locator, sec = null) {
console.log(`waitUntilExists deprecated:
* use 'waitForElement' to wait for element to be attached
* use 'waitForDetached to wait for element to be removed'`)
return this.waitForDetached(locator, sec)
}
/**
* {{> waitForDetached }}
*/
async waitForDetached(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.not(EC.presenceOf(el)), aSec * 1000).catch(err => {
if (err.message && err.message.indexOf('Wait timed out after') > -1) {
throw new Error(`element (${JSON.stringify(locator)}) still on page after ${sec} sec`)
} else throw err
})
}
/**
* Waits for element to become clickable for number of seconds.
*
* ```js
* I.waitForClickable('#link');
* ```
*/
async waitForClickable(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.elementToBeClickable(el), aSec * 1000)
}
/**
* {{> waitForVisible }}
*/
async waitForVisible(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.visibilityOf(el), aSec * 1000)
}
/**
* {{> waitToHide }}
*/
async waitToHide(locator, sec = null) {
return this.waitForInvisible(locator, sec)
}
/**
* {{> waitForInvisible }}
*/
async waitForInvisible(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.invisibilityOf(el), aSec * 1000)
}
async waitForStalenessOf(locator, sec = null) {
console.log(`waitForStalenessOf deprecated.
* Use waitForDetached to wait for element to be removed from page
* Use waitForInvisible to wait for element to be hidden on page`)
return this.waitForInvisible(locator, sec)
}
/**
* {{> waitNumberOfVisibleElements }}
*/
async waitNumberOfVisibleElements(locator, num, sec = null) {
function visibilityCountOf(loc, expectedCount) {
return function () {
return global.element
.all(loc)
.filter(el => el.isDisplayed())
.count()
.then(count => count === expectedCount)
}
}
const aSec = sec || this.options.waitForTimeoutInSeconds
const guessLoc = guessLocator(locator) || global.by.css(locator)
return this.browser.wait(visibilityCountOf(guessLoc, num), aSec * 1000).catch(() => {
throw Error(`The number of elements (${new Locator(locator)}) is not ${num} after ${aSec} sec`)
})
}
/**
* {{> waitForEnabled }}
*/
async waitForEnabled(locator, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const el = global.element(guessLocator(locator) || global.by.css(locator))
return this.browser.wait(EC.elementToBeClickable(el), aSec * 1000).catch(() => {
throw Error(`element (${new Locator(locator)}) still not enabled after ${aSec} sec`)
})
}
/**
* {{> waitForValue }}
*/
async waitForValue(field, value, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const valueToBeInElementValue = loc => {
return async () => {
const els = await findFields(this.browser, loc)
if (!els) {
return false
}
const values = await Promise.all(els.map(el => el.getAttribute('value')))
return values.filter(part => part.indexOf(value) >= 0).length > 0
}
}
return this.browser.wait(valueToBeInElementValue(field, value), aSec * 1000).catch(() => {
throw Error(`element (${field}) is not in DOM or there is no element(${field}) with value "${value}" after ${aSec} sec`)
})
}
/**
* {{> waitForFunction }}
*/
async waitForFunction(fn, argsOrSec = null, sec = null) {
let args = []
if (argsOrSec) {
if (Array.isArray(argsOrSec)) {
args = argsOrSec
} else if (typeof argsOrSec === 'number') {
sec = argsOrSec
}
}
const aSec = sec || this.options.waitForTimeoutInSeconds
return this.browser.wait(() => this.browser.executeScript.call(this.browser, fn, ...args), aSec * 1000)
}
/**
* {{> waitInUrl }}
*/
async waitInUrl(urlPart, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const waitTimeout = aSec * 1000
return this.browser.wait(EC.urlContains(urlPart), waitTimeout).catch(async e => {
const currUrl = await this.browser.getCurrentUrl()
if (/wait timed out after/i.test(e.message)) {
throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
} else {
throw e
}
})
}
/**
* {{> waitUrlEquals }}
*/
async waitUrlEquals(urlPart, sec = null) {
const aSec = sec || this.options.waitForTimeoutInSeconds
const waitTimeout = aSec * 1000
const baseUrl = this.options.url
if (urlPart.indexOf('http') < 0) {
urlPart = baseUrl + urlPart
}
return this.browser.wait(EC.urlIs(urlPart), waitTimeout).catch(async e => {
const currUrl = await this.browser.getCurrentUrl()
if (/wait timed out after/i.test(e.message)) {
throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
} else {
throw e
}
})
}
/**
* {{> waitForText }}
*/
async waitForText(text, sec = null, context = null) {
if (!context) {
context = this.context
}
const el = global.element(guessLocator(context) || global.by.css(context))
const aSec = sec || this.options.waitForTimeoutInSeconds
return this.browser.wait(EC.textToBePresentInElement(el, text), aSec * 1000)
}
// ANGULAR SPECIFIC
/**
* Moves to url
*/
moveTo(path) {
return this.browser.setLocation(path)
}
/**
* {{> refreshPage }}
*/
refreshPage() {
return this.browser.refresh()
}
/**
* Reloads page
*/
refresh() {
console.log('Deprecated in favor of refreshPage')
return this.browser.refresh()
}
/**
* {{> scrollTo }}
*/
async scrollTo(locator, offsetX = 0, offsetY = 0) {
if (typeof locator === 'number' && typeof offsetX === 'number') {
offsetY = offsetX
offsetX = locator
locator = null
}
if (locator) {
const res = await this._locate(locator, true)
if (!res || res.length === 0) {
return truth(`elements of ${new Locator(locator)}`, 'to be seen').assert(false)
}
const elem = res[0]
const location = await elem.getLocation()
return this.executeScript(
function (x, y) {
return window.scrollTo(x, y)
},
location.x + offsetX,
location.y + offsetY,
)
}
return this.executeScript(
function (x, y) {
return window.scrollTo(x, y)
},
offsetX,
offsetY,
)
}
/**
* {{> scrollPageToTop }}
*/
async scrollPageToTop() {
return this.executeScript('window.scrollTo(0, 0);')
}
/**
* {{> scrollPageToBottom }}
*/
async scrollPageToBottom() {
return this.executeScript(function () {
const body = document.body
const html = document.documentElement
window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight))
})
}
/**
* {{> grabPageScrollPosition }}
*/
async grabPageScrollPosition() {
function getScrollPosition() {
return {
x: window.pageXOffset,
y: window.pageYOffset,
}
}
return this.executeScript(getScrollPosition)
}
/**
* Injects Angular module.
*
* ```js
* I.haveModule('modName', function() {
* angular.module('modName', []).value('foo', 'bar');
* });
* ```
*/
haveModule(modName, fn) {
return this.browser.addMockModule(modName, fn)
}
/**
* Removes mocked Angular module. If modName not specified - clears all mock modules.
*
* ```js
* I.resetModule(); // clears all
* I.resetModule('modName');
* ```
*/
resetModule(modName) {
if (!modName) {
return this.browser.clearMockModules()
}
return this.browser.removeMockModule(modName)
}
/**
* {{> setCookie }}
*/
setCookie(cookie) {
return this.browser.manage().addCookie(cookie)
}
}
module.exports = Protractor
async function findCheckable(client, locator) {
const matchedLocator = guessLocator(locator)
if (matchedLocator) {
return client.findElements(matchedLocator)
}
const literal = xpathLocator.literal(locator)
let els = await client.findElements(global.by.xpath(Locator.checkable.byText(literal)))
if (els.length) {
return els
}
els = await client.findElements(global.by.xpath(Locator.checkable.byName(literal)))
if (els.length) {
return els
}
return client.findElements(global.by.css(locator))
}
function withStrictLocator(locator) {
locator = new Locator(locator)
if (locator.isAccessibilityId()) return withAccessiblitiyLocator.call(this, locator.value)
return locator.simplify()
}
function withAccessiblitiyLocator(locator) {
if (this.isWeb === false) {
return `accessibility id:${locator.slice(1)}`
}
return `[aria-label="${locator.slice(1)}"]`
// hook before webdriverio supports native ~ locators in web
}
async function findFields(client, locator) {
const matchedLocator = guessLocator(locator)
if (matchedLocator) {
return client.findElements(matchedLocator)
}
const literal = xpathLocator.literal(locator)
let els = await client.findElements(global.by.xpath(Locator.field.labelEquals(literal)))
if (els.length) {
return els
}
els = await client.findElements(global.by.xpath(Locator.field.labelContains(literal)))
if (els.length) {
return els
}
els = await client.findElements(global.by.xpath(Locator.field.byName(literal)))
if (els.length) {
return els
}
return client.findElements(global.by.css(locator))
}
async function proceedSee(assertType, text, context) {
let description
let locator
if (!context) {
if (this.context === this.options.rootElement) {
locator = guessLocator(this.context) || global.by.css(this.context)
description = 'web application'
} else {
// inside within block
locator = global.by.xpath('.//*')
description = `current context ${new Locator(context).toString()}`
}
} else {
locator = guessLocator(context) || global.by.css(context)
description = `element ${new Locator(context).toString()}`
}
const enableSmartWait =