UNPKG

@freelancercom/blue-harvest

Version:
381 lines 17.1 kB
"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