UNPKG

@serenity-js/web

Version:

Serenity/JS Screenplay Pattern library offering a flexible, web driver-agnostic approach for interacting with web-based user interfaces and components, suitable for various testing contexts

389 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExecuteScriptWithArguments = exports.ExecuteScript = void 0; const core_1 = require("@serenity-js/core"); const io_1 = require("@serenity-js/core/lib/io"); const model_1 = require("@serenity-js/core/lib/model"); const abilities_1 = require("../abilities"); /** * Instructs an [actor](https://serenity-js.org/api/core/class/Actor/) who has the [ability](https://serenity-js.org/api/core/class/Ability/) to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/) * to inject a script into the browser and execute it in the context of the current browser tab. * * ## Learn more * * - [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/) * - [`LastScriptExecution.result`](https://serenity-js.org/api/web/class/LastScriptExecution/#result) * * @group Activities */ class ExecuteScript { /** * Instantiates a version of this [`Interaction`](https://serenity-js.org/api/core/class/Interaction/) * configured to load a script from `sourceUrl`. * * @param sourceUrl * The URL to load the script from */ static from(sourceUrl) { return new ExecuteScriptFromUrl(sourceUrl); } /** * Instructs an [actor](https://serenity-js.org/api/core/class/Actor/) who has the [ability](https://serenity-js.org/api/core/class/Ability/) to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/) * to execute an asynchronous script within the context of the current browser tab. * * 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 [`PageElement`](https://serenity-js.org/api/web/class/PageElement/). * 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 [`ExecuteScript.sync`](https://serenity-js.org/api/web/class/ExecuteScript/#sync), * 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]`. * * If the script invokes the `callback` with a return value, this will be made available * via the [`LastScriptExecution.result`](https://serenity-js.org/api/web/class/LastScriptExecution/#result). * * **Please note** that in order to signal an error in the `script` you need to throw an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) * instead of passing it to the callback function. * * #### Executing an async script * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript } from '@serenity-js/web' * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(` * var callback = arguments[arguments.length - 1] * * // do stuff * * callback(result) * `) * ) * ``` * * #### Executing async script as function * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript } from '@serenity-js/web' * * const MyPage = { * header: () => * PageElement.located(By.css('h1')).describedAs('header'), * } * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(function getText(header, callback) { * callback(header.innerText) * }).withArguments(MyPage.header()) * ) * ``` * * #### Passing arguments to an async script * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript } from '@serenity-js/web' * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(` * var name = arguments[0]; * var age = arguments[1]; * var callback = arguments[arguments.length - 1] * * // do stuff * * callback(result) * `).withArguments('Bob', 24) * ) * ``` * * #### Passing PageElement arguments to an async script * * Serenity/JS automatically converts [`PageElement`](https://serenity-js.org/api/web/class/PageElement/) objects passed as arguments to the script * into their corresponding DOM elements. * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript, PageElement } from '@serenity-js/web' * * const MyPage = { * header: () => * PageElement.located(By.css('h1')).describedAs('header'), * } * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(` * var header = arguments[0] * var callback = arguments[arguments.length - 1] * * callback(header.innerText) * `).withArguments(MyPage.header()) * ) * ``` * * #### Using nested data structures containing PageElement objects * * Serenity/JS automatically converts any [`PageElement`](https://serenity-js.org/api/web/class/PageElement/) objects * contained in nested data structures passed to the script * into their corresponding DOM elements. * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript, PageElement } from '@serenity-js/web' * * const MyPage = { * header: () => * PageElement.located(By.css('h1')).describedAs('header'), * * article: () => * PageElement.located(By.css('article')).describedAs('article'), * } * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(` * var { include, exclude } = arguments[0] * var callback = arguments[arguments.length - 1] * * callback(include[0].innerText) * `).withArguments({ * include: [ MyPage.article() ], * exclude: [ MyPage.header() ], * }) * ) * ``` * * #### Learn more * - [`LastScriptExecution.result`](https://serenity-js.org/api/web/class/LastScriptExecution/#result) * * @param script * The script to be executed */ static async(script) { return new ExecuteAsynchronousScript(`#actor executes an asynchronous script`, script); } /** * Instructs an [actor](https://serenity-js.org/api/core/class/Actor/) who has the [ability](https://serenity-js.org/api/core/class/Ability/) to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/) * to execute a synchronous script within the context of the current browser tab. * * If the script returns a value, it will be made available via [`LastScriptExecution.result`](https://serenity-js.org/api/web/class/LastScriptExecution/#result). * * #### Executing a sync script as string and reading the result * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript, LastScriptExecution } from '@serenity-js/web' * import { Ensure, includes } from '@serenity-js/assertions' * * await actorCalled('Joseph') * .attemptsTo( * ExecuteScript.sync('return navigator.userAgent'), * Ensure.that(LastScriptExecution.result<string>(), includes('Chrome')), * ) * ``` * * #### Executing a sync script as function and retrieving the result * * ```ts * import { actorCalled } from '@serenity-js/core' * import { By, Enter, ExecuteScript, LastScriptExecution, PageElement } from '@serenity-js/web' * * const Checkout = { * someOfferField: () => * PageElement.located(By.id('offer-code')) * .describedAs('offer code') * * applyOfferCodeField = () => * PageElement.located(By.id('apply-offer-code')) * .describedAs('apply offer field') * } * * await actorCalled('Joseph') * .attemptsTo( * // inject JavaScript to read some property of an element * ExecuteScript.sync(function getValue(element) { * return element.value; * }).withArguments(Checkout.someOfferField()), * * // use LastScriptExecution.result() to read the value * // returned from the injected script * // and pass it to another interaction * Enter.theValue(LastScriptExecution.result<string>()).into(Checkout.applyOfferCodeField()), * ) * ``` * * #### Passing PageElement arguments to a sync script * * Serenity/JS automatically converts [`PageElement`](https://serenity-js.org/api/web/class/PageElement/) objects passed as arguments to the script * into their corresponding DOM elements. * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript, PageElement } from '@serenity-js/web' * * const MyPage = { * header: () => * PageElement.located(By.css('h1')).describedAs('header'), * } * * await actorCalled('Esti').attemptsTo( * ExecuteScript.sync(function getInnerHtml(element) { * return element.innerHTML; * }).withArguments(MyPage.header()) * ) * ``` * * #### Using nested data structures containing PageElement objects * * Serenity/JS automatically converts any [`PageElement`](https://serenity-js.org/api/web/class/PageElement/) objects * contained in nested data structures passed to the script * into their corresponding DOM elements. * * ```ts * import { actorCalled } from '@serenity-js/core' * import { ExecuteScript, PageElement } from '@serenity-js/web' * * const MyPage = { * header: () => * PageElement.located(By.css('h1')).describedAs('header'), * * article: () => * PageElement.located(By.css('article')).describedAs('article'), * } * * await actorCalled('Esti').attemptsTo( * ExecuteScript.async(function getInnerHtml(scope) { * return scope.include[0].innerHTML; * `).withArguments({ * include: [ MyPage.article() ], * exclude: [ MyPage.header() ], * }) * ) * ``` * * #### Learn more * - [`LastScriptExecution.result`](https://serenity-js.org/api/web/class/LastScriptExecution/#result) * * @param script * The script to be executed */ static sync(script) { return new ExecuteSynchronousScript(`#actor executes a synchronous script`, script); } } exports.ExecuteScript = ExecuteScript; /** * Allows for a script to be executed to be parametrised. * * ## Learn more * - [`ExecuteScript`](https://serenity-js.org/api/web/class/ExecuteScript/) * * @group Activities */ class ExecuteScriptWithArguments extends core_1.Interaction { script; args; constructor(description, script, // eslint-disable-line @typescript-eslint/ban-types args = []) { super(description, core_1.Interaction.callerLocation(5)); this.script = script; this.args = args; } /** * @inheritDoc */ async performAs(actor) { const args = await (0, io_1.asyncMap)(this.args, arg => actor.answer(arg)); await this.executeAs(actor, args); actor.collect(model_1.TextData.fromJSON({ contentType: 'text/javascript;charset=UTF-8', data: this.script.toString(), }), new model_1.Name('Script source')); } } exports.ExecuteScriptWithArguments = ExecuteScriptWithArguments; /** * @package */ class ExecuteAsynchronousScript extends ExecuteScriptWithArguments { withArguments(...args) { return new ExecuteAsynchronousScript(args.length > 0 ? (0, core_1.the) `#actor executes an asynchronous script with arguments: ${args}` : this.toString(), this.script, args); } async executeAs(actor, args) { const page = await abilities_1.BrowseTheWeb.as(actor).currentPage(); return page.executeAsyncScript(this.script, ...args); // todo: fix types } } /** * @package * * https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/JavascriptExecutor.html * https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement */ class ExecuteScriptFromUrl extends core_1.Interaction { sourceUrl; constructor(sourceUrl) { super((0, core_1.the) `#actor executes a script from ${sourceUrl}`); this.sourceUrl = sourceUrl; } /** * @inheritDoc */ async performAs(actor) { const page = await abilities_1.BrowseTheWeb.as(actor).currentPage(); const sourceUrl = await actor.answer(this.sourceUrl); return page.executeAsyncScript( /* c8 ignore start */ function executeScriptFromUrl(sourceUrl, callback) { const alreadyLoadedScripts = Array.prototype.slice .call(document.querySelectorAll('script')) .map(script => script.src); if (~alreadyLoadedScripts.indexOf(sourceUrl)) { return callback('Script from ' + sourceUrl + ' has already been loaded'); } const script = document.createElement('script'); script.addEventListener('load', function () { callback(); }); script.addEventListener('error', function () { return callback(`Couldn't load script from ${sourceUrl}`); }); script.src = sourceUrl; script.async = true; document.head.append(script); }, sourceUrl /* c8 ignore stop */ ) .then(errorMessage => { if (errorMessage) { throw new core_1.LogicError(errorMessage); } }); } } /** * @package */ class ExecuteSynchronousScript extends ExecuteScriptWithArguments { withArguments(...args) { return new ExecuteSynchronousScript(args.length > 0 ? (0, core_1.the) `#actor executes a synchronous script with arguments: ${args}` : this.toString(), this.script, args); } async executeAs(actor, args) { const page = await abilities_1.BrowseTheWeb.as(actor).currentPage(); return page.executeScript(this.script, ...args); // todo fix type } } //# sourceMappingURL=ExecuteScript.js.map