import { Ability, LogicError, UsesAbilities } from '@serenity-js/core'; import { ActionSequence, ElementArrayFinder, ElementFinder, Locator, protractor, ProtractorBrowser } from 'protractor'; import { Navigation, Options } from 'selenium-webdriver'; import { promiseOf } from '../../promiseOf'; /** * @desc * An {@link @serenity-js/core/lib/screenplay~Ability} that enables the {@link Actor} to interact with web front-ends using {@link protractor}. * * @example Using the protractor.browser * import { Actor } from '@serenity-js/core'; * import { BrowseTheWeb, Navigate, Target } from '@serenity-js/protractor' * import { Ensure, equals } from '@serenity-js/assertions'; * import { by, protractor } from 'protractor'; * * const actor = Actor.named('Wendy').whoCan( * BrowseTheWeb.using(protractor.browser), * ); * * const HomePage = { * Title: Target.the('title').located(by.css('h1')), * }; * * actor.attemptsTo( * Navigate.to(`https://serenity-js.org`), * Ensure.that(Text.of(HomePage.Title), equals('Serenity/JS')), * ); * * @see https://www.protractortest.org/ * * @public * @implements {@link @serenity-js/core/lib/screenplay~Ability} */ export class BrowseTheWeb implements Ability { /** * @private */ private lastScriptExecutionSummary: LastScriptExecutionSummary; /** * @desc * Ability to interact with web front-ends using a given protractor browser instance. * * @param {ProtractorBrowser} browser * @returns {BrowseTheWeb} */ static using(browser: ProtractorBrowser): BrowseTheWeb { return new BrowseTheWeb(browser); } /** * @desc * Used to access the Actor's ability to {@link BrowseTheWeb} from within the {@link Interaction} classes, * such as {@link Navigate}. * * @param {UsesAbilities} actor * @return {BrowseTheWeb} */ static as(actor: UsesAbilities): BrowseTheWeb { return actor.abilityTo(BrowseTheWeb); } /** * @param {ProtractorBrowser} browser * An instance of a protractor browser */ constructor(private readonly browser: ProtractorBrowser) { } /** * @desc * Navigate to the given destination and loads mock modules before Angular. * Assumes that the page being loaded uses Angular. * * @param {string} destination * @param {number?} timeoutInMillis * * @returns {Promise} */ get(destination: string, timeoutInMillis?: number): Promise { return promiseOf(this.browser.get(destination, timeoutInMillis)); } /** * @desc * Interface for navigating back and forth in the browser history. * * @returns {Navigation} */ navigate(): Navigation { return this.browser.navigate(); } /** * @desc * Interface for defining sequences of complex user interactions. * Each sequence will not be executed until `perform` is called. * * @returns {external:selenium-webdriver.ActionSequence} */ actions(): ActionSequence { return this.browser.actions(); } /** * @desc * Interface for managing browser and driver state. * * @returns {external:selenium-webdriver.Options} */ manage(): Options { /* this.browser.manage().deleteCookie(); this.browser.manage().deleteAllCookies(); return this.browser.manage().getCookie('asd'); */ return this.browser.manage(); } /** * @desc * Locates a single element identified by the locator * * @param {Locator} locator * @returns {ElementFinder} */ locate(locator: Locator): ElementFinder { return this.browser.element(locator); } /** * @desc * Locates all elements identified by the locator * * @param {Locator} locator * @returns {ElementArrayFinder} */ locateAll(locator: Locator): ElementArrayFinder { return this.browser.element.all(locator); } /** * @desc * If set to false, Protractor will not wait for Angular $http and $timeout * tasks to complete before interacting with the browser. * * This can be useful when: * - you need to switch to a non-Angular app during your tests (i.e. SSO login gateway) * - your app continuously polls an API with $timeout * * If you're not testing an Angular app, it's better to disable Angular synchronisation completely * in protractor configuration: * * @example protractor.conf.js * exports.config = { * onPrepare: function () { * return browser.waitForAngularEnabled(false); * }, * * // ... other config * }; * * @param {boolean} enable * * @returns {Promise} */ enableAngularSynchronisation(enable: boolean): Promise { return promiseOf(this.browser.waitForAngularEnabled(enable)); } /** * @desc * Schedules a command to execute JavaScript in the context of the currently selected frame or window. * The script fragment will be executed as the body of an anonymous function. * If the script is provided as a function object, that function will be converted to a string for injection * into the target window. * * Any arguments provided in addition to the script will be included as script arguments and may be referenced * using the `arguments` object. Arguments may be a `boolean`, `number`, `string` or `WebElement`. * Arrays and objects may also be used as script arguments as long as each item adheres * to the types previously mentioned. * * The script may refer to any variables accessible from the current window. * Furthermore, the script will execute in the window's context, thus `document` may be used to refer * to the current document. Any local variables will not be available once the script has finished executing, * though global variables will persist. * * If the script has a return value (i.e. if the script contains a `return` statement), * then the following steps will be taken for resolving this functions return value: * * For a HTML element, the value will resolve to a WebElement * - Null and undefined return values will resolve to null * - Booleans, numbers, and strings will resolve as is * - Functions will resolve to their string representation * - For arrays and objects, each member item will be converted according to the rules above * * @example Perform a sleep in the browser under test * BrowseTheWeb.as(actor).executeAsyncScript(` * return arguments[0].tagName; * `, Target.the('header').located(by.css(h1)) * * @see https://www.protractortest.org/#/api?view=webdriver.WebDriver.prototype.executeScript * @see https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeScript-java.lang.String-java.lang.Object...- * * @param {string} description - useful for debugging * @param {string | Function} script * @param {any[]} args */ executeScript(description: string, script: string | Function, ...args: any[]) { // tslint:disable-line:ban-types return promiseOf(this.browser.executeScriptWithDescription(script, description, ...args)) .then(result => { this.lastScriptExecutionSummary = new LastScriptExecutionSummary( result, ); return result; }); } /** * @desc * Schedules a command to execute asynchronous JavaScript in the context of the currently selected frame or window. * The script fragment will be executed as the body of an anonymous function. * If the script is provided as a function object, that function will be converted to a string for injection * into the target window. * * Any arguments provided in addition to the script will be included as script arguments and may be referenced * using the `arguments` object. Arguments may be a `boolean`, `number`, `string` or `WebElement` * Arrays and objects may also be used as script arguments as long as each item adheres * to the types previously mentioned. * * Unlike executing synchronous JavaScript with {@link BrowseTheWeb#executeScript}, * scripts executed with this function must explicitly signal they are finished by invoking the provided callback. * * This callback will always be injected into the executed function as the last argument, * and thus may be referenced with `arguments[arguments.length - 1]`. * * The following steps will be taken for resolving this functions return value against * the first argument to the script's callback function: * * - For a HTML element, the value will resolve to a WebElement * - Null and undefined return values will resolve to null * - Booleans, numbers, and strings will resolve as is * - Functions will resolve to their string representation * - For arrays and objects, each member item will be converted according to the rules above * * @example Perform a sleep in the browser under test * BrowseTheWeb.as(actor).executeAsyncScript(` * var delay = arguments[0]; * var callback = arguments[arguments.length - 1]; * * window.setTimeout(callback, delay); * `, 500) * * @example Return a value asynchronously * BrowseTheWeb.as(actor).executeAsyncScript(` * var callback = arguments[arguments.length - 1]; * * callback('some return value') * `).then(value => doSomethingWithThe(value)) * * @see https://www.protractortest.org/#/api?view=webdriver.WebDriver.prototype.executeAsyncScript * @see https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeAsyncScript-java.lang.String-java.lang.Object...- * * @param {string|Function} script * @param {any[]} args */ executeAsyncScript(script: string | Function, ...args: any[]): Promise { // tslint:disable-line:ban-types return promiseOf(this.browser.executeAsyncScript(script, ...args)) .then(result => { this.lastScriptExecutionSummary = new LastScriptExecutionSummary( result, ); return result; }); // todo: should I wrap this an provide additional diagnostic information? execution time? error handling? } /** * @desc * Schedule a command to take a screenshot. The driver makes a best effort to * return a base64-encoded screenshot of the following, in order of preference: * * 1. Entire page * 2. Current window * 3. Visible portion of the current frame * 4. The entire display containing the browser * * @return {Promise} A promise that will be resolved to a base64-encoded screenshot PNG */ takeScreenshot(): Promise { return promiseOf(this.browser.takeScreenshot()); } /** * @desc * Returns the title of the current page. * * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title * * @returns {Promise} */ getTitle(): Promise { return promiseOf(this.browser.getTitle()); } /** * @desc * Returns the url of the current page. * * @returns {Promise} */ getCurrentUrl(): Promise { return promiseOf(this.browser.getCurrentUrl()); } /** * @desc * Pause the actor flow for a specified number of milliseconds. * * @returns {Promise} */ sleep(millis: number): Promise { return promiseOf(this.browser.sleep(millis)); } /** * @desc * Pause the actor flow until the condition is met or the timeout expires. * * @returns {Promise} */ wait(condition: () => Promise, timeout: number): Promise { return promiseOf(this.browser.wait(condition, timeout)); } getLastScriptExecutionResult(): any { if (! this.lastScriptExecutionSummary) { throw new LogicError(`Make sure to execute a script before checking on the result`); } return this.lastScriptExecutionSummary.result; } } /** * @package */ class LastScriptExecutionSummary { constructor(public readonly result: any) {} }