codeceptjs
Version:
Modern Era Acceptance Testing Framework for NodeJS
1,716 lines (1,526 loc) • 50.9 kB
JavaScript
let EC;
let Key;
let Button;
let ProtractorBy;
let ProtractorExpectedConditions;
const requireg = require('requireg');
const Helper = require('../helper');
const stringIncludes = require('../assert/include').includes;
const { urlEquals, equals } = require('../assert/equal');
const empty = require('../assert/empty').empty;
const truth = require('../assert/truth').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');
const path = require('path');
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.json 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["WebDriverIO"]._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);
}
_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.waitForTimeout /= 1000; // convert to seconds
return config;
}
async _init() {
process.on('unhandledRejection', (reason) => {
if (reason.message.indexOf('ECONNREFUSED') > 0) {
this.browser = null;
}
});
Runner = requireg('protractor/built/runner').Runner;
ProtractorBy = requireg('protractor').ProtractorBy;
Key = requireg('protractor').Key;
Button = requireg('protractor').Button;
ProtractorExpectedConditions = requireg('protractor').ProtractorExpectedConditions;
return Promise.resolve();
}
static _checkRequirements() {
try {
requireg('protractor');
require('assert').ok(requireg('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(test, err) {
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);
},
};
}
/**
* 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);
}
/**
* Checks that text is equal to provided one.
*
* ```js
* I.seeTextEquals('text', 'h1');
* ```
*/
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])) { // eslint-disable-line no-bitwise
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) {
return proceedSeeInField.call(this, 'assert', field, value);
}
/**
* {{> dontSeeInField }}
*/
async dontSeeInField(field, value) {
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);
}
/**
* {{> 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);
}
/**
* {{> grabTextFrom }}
*/
async grabTextFrom(locator) {
const els = await this._locate(locator);
assertElementExists(els);
const texts = [];
for (const el of els) {
texts.push(await el.getText());
}
if (texts.length === 1) return texts[0];
return texts;
}
/**
* {{> grabHTMLFrom }}
*/
async grabHTMLFrom(locator) {
const els = await this._locate(locator);
assertElementExists(els);
const html = await Promise.all(els.map((el) => {
return this.browser.executeScript('return arguments[0].innerHTML;', el);
}));
if (html.length === 1) {
return html[0];
}
return html;
}
/**
* {{> grabValueFrom }}
*/
async grabValueFrom(locator) {
const els = await findFields(this.browser, locator);
assertElementExists(els, locator, 'Field');
return els[0].getAttribute('value');
}
/**
* {{> grabCssPropertyFrom }}
*/
async grabCssPropertyFrom(locator, cssProperty) {
const els = await this._locate(locator, true);
assertElementExists(els, locator);
const values = await Promise.all(els.map(el => el.getCssValue(cssProperty)));
if (Array.isArray(values) && values.length === 1) {
return values[0];
}
return values;
}
/**
* {{> grabAttributeFrom }}
*/
async grabAttributeFrom(locator, attr) {
const els = await this._locate(locator);
assertElementExists(els);
const array = [];
for (let index = 0; index < els.length; index++) {
const el = els[index];
array.push(await el.getAttribute(attr));
}
return array.length === 1 ? array[0] : array;
}
/**
* {{> seeInTitle }}
*/
async seeInTitle(text) {
return this.browser.getTitle().then(title => stringIncludes('web page title').assert(text, title));
}
/**
* Checks that title is equal to provided one.
*
* ```js
* I.seeTitleEquals('Test title.');
* ```
*/
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 (${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 (${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 (${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 (${locator}) to have attributes ${JSON.stringify(attributes)}`).assert(missingAttributes.length, 0);
}
/**
* {{> executeScript }}
*/
async executeScript(fn) {
return this.browser.executeScript.apply(this.browser, arguments);
}
/**
* {{> executeAsyncScript }}
*/
async executeAsyncScript(fn) {
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));
}
/**
* {{> 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(() => ({ // eslint-disable-line
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();
}
/**
* Checks that the active JavaScript popup, as created by `window.alert|window.confirm|window.prompt`, contains the
* given string.
*/
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);
assertElementExists(destEl);
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);
} else 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.waitForTimeout;
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.waitForTimeout;
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.waitForTimeout;
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.waitForTimeout;
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.waitForTimeout;
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.waitForTimeout;
const guessLoc = guessLocator(locator) || global.by.css(locator);
return this.browser.wait(visibilityCountOf(guessLoc, num), aSec * 1000)
.catch((err) => {
throw Error(`The number of elements (${locator}) is not ${num} after ${aSec} sec`);
});
}
/**
* {{> waitForEnabled }}
*/
async waitForEnabled(locator, sec = null) {
const aSec = sec || this.options.waitForTimeout;
const el = global.element(guessLocator(locator) || global.by.css(locator));
return this.browser.wait(EC.elementToBeClickable(el), aSec * 1000)
.catch((err) => {
throw Error(`element (${locator}) still not enabled after ${aSec} sec`);
});
}
/**
* {{> waitForValue }}
*/
async waitForValue(field, value, sec = null) {
const aSec = sec || this.options.waitForTimeout;
const valueToBeInElementValue = (loc, expectedCount) => {
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((err) => {
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.waitForTimeout;
return this.browser.wait(() => this.browser.executeScript.call(this.browser, fn, ...args), aSec * 1000);
}
/**
* {{> waitUntil }}
*/
async waitUntil(fn, sec = null, timeoutMsg = null) {
const aSec = sec || this.options.waitForTimeout;
return this.browser.wait(fn, aSec * 1000, timeoutMsg);
}
/**
* {{> waitInUrl }}
*/
async waitInUrl(urlPart, sec = null) {
const aSec = sec || this.options.waitForTimeout;
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.waitForTimeout;
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.waitForTimeout;
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 ${locator}`, 'to be seen').assert(false);
}
const elem = res[0];
const location = await elem.getLocation();
/* eslint-disable prefer-arrow-callback */
return this.executeScript(function (x, y) { return window.scrollTo(x, y); }, location.x + offsetX, location.y + offsetY);
/* eslint-enable */
}
/* eslint-disable prefer-arrow-callback, comma-dangle */
return this.executeScript(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY);
/* eslint-enable */
}
/**
* {{> scrollPageToTop }}
*/
async scrollPageToTop() {
return this.executeScript('window.scrollTo(0, 0);');
}
/**
* {{> scrollPageToBottom }}
*/
async scrollPageToBottom() {
/* eslint-disable prefer-arrow-callback, comma-dangle */
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
));
});
/* eslint-enable */
}
/**
* {{> grabPageScrollPosition }}
*/
async grabPageScrollPosition() {
/* eslint-disable comma-dangle */
function getScrollPosition() {
return {
x: window.pageXOffset,
y: window.pageYOffset
};
}
/* eslint-enable comma-dangle */
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 = !!this.context && assertType === 'assert';
const els = await this._smartWait(() => this.browser.findElements(locator), enableSmartWait);
const promises = [];
let source = '';
els.forEach(el => promises.push(el.getText().then(elText => source += `| ${elText}`)));
await Promise.all(promises);
return stringIncludes(description)[assertType](text, source);
}
async function proceedSeeInField(assertType, field, value) {
const els = await findFields(this.browser, field);
assertElementExists(els, field, 'Field');
const el = els[0];
const tag = await el.getTagName();
const fieldVal = await el.getAttribute('value');
if (tag === 'select') {
// locate option by values and check them
const literal = xpathLocator.literal(fieldVal);
const textEl = await el.findElement(global.by.xpath(Locator.select.byValue(literal)));
const text = await textEl.getText();
return equals(`select option by ${field}`)[assertType](value, text);
}
return stringIncludes(`field by ${field}`)[assertType](value, fieldVal);
}
async function proceedIsChecked(assertType, option) {
const els = await findCheckable(this.browser, option);
assertElementExists(els, option, 'Option');
const elsSelected = [];
els.forEach(el => elsSelected.push(el.isSelected()));
const values = await Promise.all(elsSelected);
const selected = values.reduce((prev, cur) => prev || cur);
return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
}
async function findClickable(matcher, locator) {
locator = new Locator(locator);
if (!locator.isFuzzy()) {
const els = await this._locate(locator, true);
assertElementExists(els, locator.value);
return els[0];
}
const literal = xpathLocator.literal(locator.value);
const narrowLocator = Locator.clickable.narrow(literal);
let els = await matcher.findElements(global.by.xpath(narrowLocator));
if (els.length) {
return els[0];
}
els = await matcher.findElements(global.by.xpath(Locator.clickable.wide(literal)));
if (els.length) {
return els[0];
}
retu