@freelancercom/blue-harvest
Version:
protractor helpers
381 lines • 17.1 kB
JavaScript
"use strict";
/**
* Generic actions for interacting with the Pantheon UI in integration tests.
* Every action will automatically wait for the relevant elements to be
* in view and capable of interaction, and will retry upon failure.
*
* Examples of usage:
* see('Sandwich Order Form');
* under('Cheese').see('Provelone');
* under('Cheese').not.see('American');
* click('Order Sandwich');
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.go = exports.type = exports.inside = exports.below = exports.rightOf = exports.leftOf = exports.under = exports.agonizinglySlow = exports.slow = exports.fast = exports.not = exports.uploadFile = exports.mouseOver = exports.tap = exports.longPress = exports.click = exports.see = exports.find = exports.ChainedAction = exports.ActionContext = exports.Slowness = void 0;
const protractor_1 = require("protractor");
const webdriver = require("selenium-webdriver");
const url_1 = require("url");
const find_1 = require("./find");
const locator_types_1 = require("./locator_types");
const logger_1 = require("./logger");
const FAST_FIND_TIMEOUT = 1 * 1000;
const FIND_TIMEOUT = 30 * 1000;
const SLOW_FIND_TIMEOUT = 90 * 1000;
const AGONIZINGLY_SLOW_FIND_TIMEOUT = 10 * 60 * 1000;
const PAGE_LOAD_TIMEOUT = 60 * 1000;
// Disable the promise manager if it hasn't already been. We rely on Protractor
// to set this when it reads the config file, and in some situations this code
// may run before that happens.
if (webdriver.promise.USE_PROMISE_MANAGER) {
webdriver.promise.USE_PROMISE_MANAGER = false;
console.warn('Disabling the WebDriver promise manager. This might cause ' +
'unexpected behavior. Please make sure you set ' +
'SELENIUM_PROMISE_MANAGER=false in your Protractor config.');
}
/**
* An enum declaring how slow a test should be.
* Don't use this directly.
*/
var Slowness;
(function (Slowness) {
Slowness[Slowness["FAST"] = 0] = "FAST";
Slowness[Slowness["REGULAR"] = 1] = "REGULAR";
Slowness[Slowness["SLOW"] = 2] = "SLOW";
Slowness[Slowness["AGONIZINGLY_SLOW"] = 3] = "AGONIZINGLY_SLOW";
})(Slowness = exports.Slowness || (exports.Slowness = {}));
/**
* Map between slowness and what that actually means.
*/
const SLOWNESS_MAP = new Map([
[Slowness.FAST, { description: 'fast.', timeout: FAST_FIND_TIMEOUT }],
[Slowness.REGULAR, { description: '', timeout: FIND_TIMEOUT }],
[Slowness.SLOW, { description: 'slow.', timeout: SLOW_FIND_TIMEOUT }],
[
Slowness.AGONIZINGLY_SLOW, {
description: 'agonizinglySlow.',
timeout: AGONIZINGLY_SLOW_FIND_TIMEOUT
}
],
]);
/**
* Information necessary to determine how to find an element.
*/
class ActionContext {
constructor(locators, slow, wantZero) {
this.locators = locators;
this.slow = slow;
this.wantZero = wantZero;
}
static default() {
return new ActionContext([], Slowness.REGULAR, false);
}
addLocator(position, locator) {
const newLocators = [...this.locators, { position, locator }];
return new ActionContext(newLocators, this.slow, this.wantZero);
}
setSlow(newSlow) {
return new ActionContext(this.locators, newSlow, this.wantZero);
}
setNot(newNot) {
return new ActionContext(this.locators, this.slow, newNot);
}
}
exports.ActionContext = ActionContext;
/**
* A ChainedAction captures information on how to interact with the page
* under test. The class contain modifier methods, e.g. leftOf and below,
* which return a new ChainedAction with additional context. It also contains
* action methods, e.g. see and click, which perform the action and complete
* the chain.
*/
class ChainedAction {
// ActionContext is treated as immutable so that we can reuse chained actions.
constructor(context) {
this.context = context;
this.not = { see: this.notSee.bind(this) };
}
/**
* Specify that the element to be found must be rendered below AND in the same
* vertical space as the element found by this locator.
*/
under(locator) {
return new ChainedAction(this.context.addLocator(locator_types_1.Position.UNDER, locator));
}
/**
* Specify that the element to be found must be rendered below the element
* found by this locator.
*/
below(locator) {
return new ChainedAction(this.context.addLocator(locator_types_1.Position.BELOW, locator));
}
/**
* Specify that the element to be found must be rendered inside the element
* found by this locator.
*/
inside(locator) {
return new ChainedAction(this.context.addLocator(locator_types_1.Position.INSIDE, locator));
}
/**
* Specify that the element to be found must rendered to the right of the
* element found by this locator.
*/
rightOf(locator) {
return new ChainedAction(this.context.addLocator(locator_types_1.Position.RIGHTOF, locator));
}
/**
* Specify that the element to be found must be rendered to the left of the
* element found by the locator.
*/
leftOf(locator) {
return new ChainedAction(this.context.addLocator(locator_types_1.Position.LEFTOF, locator));
}
notSee(locator, options) {
return new ChainedAction(this.context.setNot(true)).see(locator, options);
}
description() {
let text = '';
text += (this.context.wantZero ? 'not.' : '');
text += SLOWNESS_MAP.get(this.context.slow).description;
for (const modifier of this.context.locators) {
text += `${modifier.position}(${this.pretty(modifier.locator)}).`;
}
return text;
}
pretty(loc) {
if (typeof (loc) === 'string') {
return `"${loc}"`;
}
else {
return loc.toString();
}
}
timeout() {
return SLOWNESS_MAP.get(this.context.slow).timeout;
}
getElement(locator, description, options) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield (0, find_1.retryingFind)(this.context.addLocator(locator_types_1.Position.GLOBAL, locator).locators, this.timeout(), description, Object.assign(Object.assign({}, options), { allowUnseen: true }));
if (response === true) {
throw new Error('An element is expected, but the client side script did ' +
'not return an element. This should never happen.');
}
return response;
});
}
/**
* Returns a WebElement from the given locator and satisfying the
* current context, or null if no element was found.
* Only use this method if you need to use the returned WebElement, otherwise
* prefer `see`.
* Note that this method, unlike other actions, does not throw if the element
* is not found.
*/
find(locator, options) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}find(${this.pretty(locator)})`;
(0, logger_1.log)(description);
try {
return yield this.getElement(locator, description, options);
}
catch (e) {
if (e.message.startsWith(`Failed to find ${description}`)) {
return null;
}
throw e;
}
});
}
/**
* Returns true if an element with the given locator and satisfying the
* current context exists. Throws an error if an element cannot be found.
*/
see(locator, options) {
return __awaiter(this, void 0, void 0, function* () {
const descriptionArgs = options ?
`${this.pretty(locator)}, ${JSON.stringify(options)}` :
this.pretty(locator);
const description = `${this.description()}see(${descriptionArgs})`;
(0, logger_1.log)(description);
const findOptions = Object.assign({ wantZero: this.context.wantZero }, options);
const response = yield (0, find_1.retryingFind)(this.context.addLocator(locator_types_1.Position.GLOBAL, locator).locators, this.timeout(), description, findOptions);
return !!response;
});
}
/**
* Finds and clicks on the first element with the given locator and satisfying
* the current context. Throws an error if an element cannot be found.
*/
click(locator) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}click(${this.pretty(locator)})`;
(0, logger_1.log)(description);
const response = yield this.getElement(locator, description);
try {
yield response.click();
}
catch (e) {
// If the error is due to something masked by an <input>, click on that
// location on the page anyway. This allows us to click on <input>
// elements using Material components which hide the label under the
// <input>.
if (/Other element would receive the click/.test(e.message)) {
console.log('Element is masked, trying to click through..');
(0, logger_1.log)(e.message);
// TODO(ralphj): We should be able to use the shorthand below,
// but a bug in webdriver bindings causes issues with this and the
// control flow:
// await browser.actions().click(response).perform();
yield protractor_1.browser.actions().move({ origin: response }).perform();
yield protractor_1.browser.actions().click().perform();
// Don't leave the mouse on a clickable element because there
// might be a hover event implemented on the element that triggers
// the event and causes unwanted test results.
yield protractor_1.browser.actions().move({ x: 0, y: 0 }).perform();
}
else {
throw e;
}
}
yield protractor_1.browser.waitForAngular();
});
}
/**
* Do a long press on the first element with the given locator and satisfying
* the current context. Throws an error if the element cannot be found.
* This function is used in mobile testing with simulated mobile device.
*/
longPress(locator) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}longPress(${this.pretty(locator)})`;
(0, logger_1.log)(description);
const element = yield this.getElement(locator, description);
// Chrome dev tools will show context menu on long press, which does not
// happen on real mobile device. Disable the context menu.
// https://stackoverflow.com/questions/41060472/how-to-disable-the-context-menu-on-long-press-when-using-device-mode-in-chrome
yield protractor_1.browser.executeScript(`
window.pantheonTestOriginalOnContextMenuHandler = window.oncontextmenu;
window.oncontextmenu = () => false;
`);
yield protractor_1.browser.touchActions().longPress(element).perform();
yield protractor_1.browser.executeScript(`
window.oncontextmenu = window.pantheonTestOriginalOnContextMenuHandler;
delete window.pantheonTestOriginalOnContextMenuHandler;
`);
yield protractor_1.browser.waitForAngular();
});
}
/**
* Taps on the first element with the given locator and satisfying
* the current context. Throws an error if the element cannot be found.
* This function is used in mobile testing with simulated mobile device.
*/
tap(locator) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}tap(${this.pretty(locator)})`;
(0, logger_1.log)(description);
const element = yield this.getElement(locator, description);
yield protractor_1.browser.touchActions().tap(element).perform();
yield protractor_1.browser.waitForAngular();
});
}
/**
* Mouse over the first element with the given locator and satisfying the
* current context. Throws an error if an element cannot be found.
*/
mouseOver(locator) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}mouseOver(${this.pretty(locator)})`;
(0, logger_1.log)(description);
const element = yield this.getElement(locator, description);
yield protractor_1.browser.actions().move({ origin: element }).perform();
yield protractor_1.browser.waitForAngular();
});
}
/**
* Type the given filepath into the first input[type="file"] element
* satisfying the current context, allowing to upload files through standard
* HTML file inputs
*/
uploadFile(filepath) {
return __awaiter(this, void 0, void 0, function* () {
const description = `${this.description()}uploadFile(${this.pretty(filepath)})`;
(0, logger_1.log)(description);
const element = yield this.getElement(protractor_1.by.xpath('//input[@type="file"]'), description, { allowCovered: true });
yield element.sendKeys(filepath);
yield protractor_1.browser.waitForAngular();
});
}
}
exports.ChainedAction = ChainedAction;
const defaultAction = ActionContext.default();
const baseAction = new ChainedAction(defaultAction);
exports.find = baseAction.find.bind(baseAction);
exports.see = baseAction.see.bind(baseAction);
exports.click = baseAction.click.bind(baseAction);
exports.longPress = baseAction.longPress.bind(baseAction);
exports.tap = baseAction.tap.bind(baseAction);
exports.mouseOver = baseAction.mouseOver.bind(baseAction);
exports.uploadFile = baseAction.uploadFile.bind(baseAction);
exports.not = baseAction.not;
exports.fast = new ChainedAction(defaultAction.setSlow(Slowness.FAST));
exports.slow = new ChainedAction(defaultAction.setSlow(Slowness.SLOW));
exports.agonizinglySlow = new ChainedAction(defaultAction.setSlow(Slowness.AGONIZINGLY_SLOW));
exports.under = baseAction.under.bind(baseAction);
exports.leftOf = baseAction.leftOf.bind(baseAction);
exports.rightOf = baseAction.rightOf.bind(baseAction);
exports.below = baseAction.below.bind(baseAction);
exports.inside = baseAction.inside.bind(baseAction);
/**
* Types text into the browser (into the currently active element).
*
* Usage:
* below('Description').click(by.css('textarea'));
* type('some text');
*/
function type(text) {
return __awaiter(this, void 0, void 0, function* () {
const description = `type(${text})`;
(0, logger_1.log)(description);
const element = yield protractor_1.browser.driver.switchTo().activeElement();
yield element.sendKeys(text);
yield protractor_1.browser.waitForAngular();
});
}
exports.type = type;
/**
* Navigate to a page in Pantheon.
* Usage:
* go('/compute/instances');
* go('/start?tutorial=quickstart');
*/
function go(path) {
return __awaiter(this, void 0, void 0, function* () {
const urlObject = new url_1.URL(path, 'https://dummy');
// Add cache invalidation param if set in Protractor config
const cacheBustingParam = protractor_1.browser.params.cacheBustingParam;
if (cacheBustingParam) {
urlObject.searchParams.set(cacheBustingParam, Date.now().toString());
}
// Add custom params if set in Protractor config
const customQueryParams = protractor_1.browser.params.customQueryParams;
if (customQueryParams) {
for (const customQueryParamName in customQueryParams) {
urlObject.searchParams.set(customQueryParamName, customQueryParams[customQueryParamName]);
}
}
const navigatePath = `${urlObject.pathname}${urlObject.search}`;
(0, logger_1.log)(`go(${navigatePath})`);
yield protractor_1.browser.get(navigatePath, PAGE_LOAD_TIMEOUT);
});
}
exports.go = go;
//# sourceMappingURL=actions.js.map