UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

1,754 lines (1,554 loc) 52.9 kB
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 =