@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
JavaScript
;
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