Socket
Socket
Sign inDemoInstall

@serenity-js/web

Package Overview
Dependencies
Maintainers
1
Versions
118
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@serenity-js/web - npm Package Compare versions

Comparing version 3.0.0-rc.19 to 3.0.0-rc.20

lib/errors/BrowserWindowClosedError.d.ts

2

lib/errors/index.d.ts

@@ -0,1 +1,3 @@

export * from './BrowserWindowClosedError';
export * from './CookieMissingError';
export * from './ModalDialogObstructsScreenshotError';

@@ -17,3 +17,5 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./BrowserWindowClosedError"), exports);
__exportStar(require("./CookieMissingError"), exports);
__exportStar(require("./ModalDialogObstructsScreenshotError"), exports);
//# sourceMappingURL=index.js.map

4

lib/expectations/ElementExpectation.d.ts

@@ -28,4 +28,4 @@ import { Expectation } from '@serenity-js/core';

* @example <caption>Using an expectation in a synchronisation statement</caption>
* import { actorCalled, Duration } from '@serenity-js/core';
* import { By, PageElement, Wait } from '@serenity-js/web';
* import { actorCalled, Duration, Wait } from '@serenity-js/core';
* import { By, PageElement } from '@serenity-js/web';
*

@@ -32,0 +32,0 @@ * const submitButton = () =>

@@ -30,4 +30,4 @@ "use strict";

* @example <caption>Using an expectation in a synchronisation statement</caption>
* import { actorCalled, Duration } from '@serenity-js/core';
* import { By, PageElement, Wait } from '@serenity-js/web';
* import { actorCalled, Duration, Wait } from '@serenity-js/core';
* import { By, PageElement } from '@serenity-js/web';
*

@@ -52,11 +52,6 @@ * const submitButton = () =>

const pageElement = await actor.answer(actual);
try {
const result = await fn(pageElement);
return result
? new core_1.ExpectationMet(this.toString(), undefined, pageElement)
: new core_1.ExpectationNotMet(this.toString(), undefined, pageElement);
}
catch (error) {
return new core_1.ExpectationNotMet(`${description} (${error.message})`, undefined, pageElement);
}
const result = await fn(pageElement);
return result
? new core_1.ExpectationMet(this.toString(), undefined, pageElement)
: new core_1.ExpectationNotMet(this.toString(), undefined, pageElement);
});

@@ -63,0 +58,0 @@ this.fn = fn;

@@ -13,4 +13,4 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export declare function isActive(): Expectation<PageElement>;

@@ -15,3 +15,3 @@ "use strict";

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -18,0 +18,0 @@ // todo: isFocused?

@@ -18,4 +18,4 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export declare function isClickable(): Expectation<PageElement>;

@@ -24,3 +24,3 @@ "use strict";

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -27,0 +27,0 @@ function isClickable() {

@@ -12,4 +12,4 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export declare function isEnabled(): Expectation<PageElement>;

@@ -14,3 +14,3 @@ "use strict";

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -17,0 +17,0 @@ function isEnabled() {

@@ -12,4 +12,4 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export declare function isSelected(): Expectation<PageElement>;

@@ -16,3 +16,3 @@ "use strict";

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -19,0 +19,0 @@ function isSelected() {

@@ -15,4 +15,4 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export declare function isVisible(): Expectation<PageElement>;

@@ -17,3 +17,3 @@ "use strict";

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -20,0 +20,0 @@ function isVisible() {

@@ -0,5 +1,32 @@

/**
* @desc
* Basic meta-data about the browser used in the test.
*
* @public
*/
export interface BrowserCapabilities {
/**
* @desc
* Name of the operating system platform the test is executed on, like `darwin`, `linux` or `windows`.
*
* @type {string | undefined}
* @public
*/
platformName?: string;
/**
* @desc
* Name of the Web browser the test is executed in, like `chrome`, `firefox` or `safari`.
*
* @type {string | undefined}
* @public
*/
browserName?: string;
/**
* @desc
* Version number of the browser the test is executed in.
*
* @type {string | undefined}
* @public
*/
browserVersion?: string;
}

@@ -1,7 +0,6 @@

import { Ability, Duration, UsesAbilities } from '@serenity-js/core';
import { Key } from '../../input';
import { Cookie, CookieData, Locator, ModalDialog, Page, Selector } from '../models';
import { Ability, UsesAbilities } from '@serenity-js/core';
import { BrowsingSession, Page } from '../models';
import { BrowserCapabilities } from './BrowserCapabilities';
export declare abstract class BrowseTheWeb<Native_Element_Type = any, Native_Root_Element_Type = unknown> implements Ability {
protected locators: Map<new (...args: unknown[]) => Selector, (selector: Selector) => Locator<Native_Element_Type, Native_Root_Element_Type>>;
export declare abstract class BrowseTheWeb<Native_Element_Type = any> implements Ability {
protected readonly session: BrowsingSession<Page<Native_Element_Type>>;
/**

@@ -16,17 +15,16 @@ * @desc

*/
static as<NET = any, NRET = unknown>(actor: UsesAbilities): BrowseTheWeb<NET, NRET>;
protected constructor(locators: Map<new (...args: unknown[]) => Selector, (selector: Selector) => Locator<Native_Element_Type, Native_Root_Element_Type>>);
abstract navigateTo(destination: string): Promise<void>;
abstract navigateBack(): Promise<void>;
abstract navigateForward(): Promise<void>;
abstract reloadPage(): Promise<void>;
abstract waitFor(duration: Duration): Promise<void>;
abstract waitUntil(condition: () => boolean | Promise<boolean>, timeout: Duration): Promise<void>;
locate(selector: Selector): Locator<Native_Element_Type, Native_Root_Element_Type>;
static as<NET = any>(actor: UsesAbilities): BrowseTheWeb<NET>;
/**
* @param {BrowsingSession<Page>} session
*
* @protected
*/
protected constructor(session: BrowsingSession<Page<Native_Element_Type>>);
/**
* @desc
* Returns basic meta-data about the browser associated with this ability.
*
* @returns {Promise<BrowserCapabilities>}
*/
abstract browserCapabilities(): Promise<BrowserCapabilities>;
abstract sendKeys(keys: Array<Key | string>): Promise<void>;
abstract executeScript<Result, InnerArguments extends any[]>(script: string | ((...parameters: InnerArguments) => Result), ...args: InnerArguments): Promise<Result>;
abstract executeAsyncScript<Result, Parameters extends any[]>(script: string | ((...args: [...parameters: Parameters, callback: (result: Result) => void]) => void), ...args: Parameters): Promise<Result>;
abstract lastScriptExecutionResult<R = any>(): R;
abstract takeScreenshot(): Promise<string>;
/**

@@ -38,3 +36,3 @@ * @desc

*/
abstract currentPage(): Promise<Page>;
currentPage(): Promise<Page<Native_Element_Type>>;
/**

@@ -47,7 +45,3 @@ * @desc

*/
abstract allPages(): Promise<Array<Page>>;
abstract cookie(name: string): Promise<Cookie>;
abstract setCookie(cookieData: CookieData): Promise<void>;
abstract deleteAllCookies(): Promise<void>;
abstract modalDialog(): Promise<ModalDialog>;
allPages(): Promise<Array<Page<Native_Element_Type>>>;
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowseTheWeb = void 0;
const core_1 = require("@serenity-js/core");
class BrowseTheWeb {
constructor(locators) {
this.locators = locators;
/**
* @param {BrowsingSession<Page>} session
*
* @protected
*/
constructor(session) {
this.session = session;
}

@@ -21,12 +25,23 @@ /**

}
locate(selector) {
for (const [type, locatorFactory] of this.locators) {
if (selector instanceof type) {
return locatorFactory(selector);
}
}
throw new core_1.LogicError((0, core_1.f) `${selector} is not supported by ${this.constructor.name}`);
/**
* @desc
* Returns a {@link Page} representing the currently active top-level browsing context.
*
* @returns {Promise<Page>}
*/
async currentPage() {
return this.session.currentPage();
}
/**
* @desc
* Returns an array of {@link Page} objects representing all the available
* top-level browsing context, e.g. all the open browser tabs.
*
* @returns {Promise<Array<Page>>}
*/
allPages() {
return this.session.allPages();
}
}
exports.BrowseTheWeb = BrowseTheWeb;
//# sourceMappingURL=BrowseTheWeb.js.map

@@ -78,3 +78,4 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

performAs(actor: UsesAbilities & AnswersQuestions): Promise<void>;
private ensureElementCanBeCleared;
private capitaliseFirstLetter;
}

@@ -5,3 +5,2 @@ "use strict";

const core_1 = require("@serenity-js/core");
const io_1 = require("@serenity-js/core/lib/io");
const PageElementInteraction_1 = require("./PageElementInteraction");

@@ -56,3 +55,3 @@ /**

constructor(field) {
super((0, io_1.formatted) `#actor clears the value of ${field}`);
super((0, core_1.d) `#actor clears the value of ${field}`);
this.field = field;

@@ -88,8 +87,18 @@ }

const element = await this.resolve(actor, this.field);
const value = await element.value();
if (value === null || value === undefined) {
throw new core_1.LogicError(this.capitaliseFirstLetter((0, io_1.formatted) `${this.field} doesn't seem to have a 'value' attribute that could be cleared.`));
}
await this.ensureElementCanBeCleared(element);
return element.clearValue();
}
async ensureElementCanBeCleared(element) {
let threwError, hasValueAttribute = false;
try {
const value = await element.value();
hasValueAttribute = value !== null && value !== undefined;
}
catch (error) {
threwError = error;
}
if (!hasValueAttribute || threwError) {
throw new core_1.LogicError(this.capitaliseFirstLetter((0, core_1.d) `${this.field} doesn't seem to have a 'value' attribute that could be cleared.`), threwError);
}
}
capitaliseFirstLetter(text) {

@@ -96,0 +105,0 @@ return text.charAt(0).toUpperCase() + text.slice(1);

@@ -81,3 +81,3 @@ "use strict";

await element.scrollIntoView();
return element.click();
await element.click();
}

@@ -84,0 +84,0 @@ }

@@ -38,4 +38,4 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

* @example <caption>Double-clicking on an element</caption>
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text, Wait } from '@serenity-js/webdriverio';
* import { actorCalled, Wait } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text } from '@serenity-js/webdriverio';
* import { Ensure, equals, not } from '@serenity-js/assertions';

@@ -42,0 +42,0 @@ *

@@ -40,4 +40,4 @@ "use strict";

* @example <caption>Double-clicking on an element</caption>
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text, Wait } from '@serenity-js/webdriverio';
* import { actorCalled, Wait } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text } from '@serenity-js/webdriverio';
* import { Ensure, equals, not } from '@serenity-js/assertions';

@@ -44,0 +44,0 @@ *

@@ -225,4 +225,5 @@ "use strict";

}
executeAs(actor, args) {
return abilities_1.BrowseTheWeb.as(actor).executeAsyncScript(this.script, ...args); // todo: fix types
async executeAs(actor, args) {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.executeAsyncScript(this.script, ...args); // todo: fix types
}

@@ -258,5 +259,5 @@ toString() {

*/
performAs(actor) {
return abilities_1.BrowseTheWeb.as(actor)
.executeAsyncScript(
async performAs(actor) {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.executeAsyncScript(
/* istanbul ignore next */

@@ -304,4 +305,5 @@ function executeScriptFromUrl(sourceUrl, callback) {

}
executeAs(actor, args) {
return abilities_1.BrowseTheWeb.as(actor).executeScript(this.script, ...args); // todo fix type
async executeAs(actor, args) {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.executeScript(this.script, ...args); // todo fix type
}

@@ -308,0 +310,0 @@ toString() {

@@ -47,3 +47,3 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

export declare class Hover extends PageElementInteraction {
private readonly target;
private readonly element;
/**

@@ -60,6 +60,6 @@ * @desc

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be hovered over
*/
constructor(target: Answerable<PageElement>);
constructor(element: Answerable<PageElement>);
/**

@@ -66,0 +66,0 @@ * @desc

@@ -50,8 +50,8 @@ "use strict";

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be hovered over
*/
constructor(target) {
super((0, io_1.formatted) `#actor hovers the mouse over ${target}`);
this.target = target;
constructor(element) {
super((0, io_1.formatted) `#actor hovers the mouse over ${element}`);
this.element = element;
}

@@ -85,3 +85,3 @@ /**

async performAs(actor) {
const element = await this.resolve(actor, this.target);
const element = await this.resolve(actor, this.element);
return element.hoverOver();

@@ -88,0 +88,0 @@ }

@@ -16,2 +16,1 @@ export * from './Clear';

export * from './TakeScreenshot';
export * from './Wait';

@@ -32,3 +32,2 @@ "use strict";

__exportStar(require("./TakeScreenshot"), exports);
__exportStar(require("./Wait"), exports);
//# sourceMappingURL=index.js.map

@@ -92,3 +92,6 @@ "use strict";

static back() {
return core_1.Interaction.where(`#actor navigates back in the browser history`, actor => abilities_1.BrowseTheWeb.as(actor).navigateBack());
return core_1.Interaction.where(`#actor navigates back in the browser history`, async (actor) => {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
await page.navigateBack();
});
}

@@ -125,3 +128,6 @@ /**

static forward() {
return core_1.Interaction.where(`#actor navigates forward in the browser history`, actor => abilities_1.BrowseTheWeb.as(actor).navigateForward());
return core_1.Interaction.where(`#actor navigates forward in the browser history`, async (actor) => {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
await page.navigateForward();
});
}

@@ -155,3 +161,6 @@ /**

static reloadPage() {
return core_1.Interaction.where(`#actor reloads the page`, actor => abilities_1.BrowseTheWeb.as(actor).reloadPage());
return core_1.Interaction.where(`#actor reloads the page`, async (actor) => {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
await page.reload();
});
}

@@ -183,9 +192,8 @@ }

*/
performAs(actor) {
return actor.answer(this.url)
.then(url => abilities_1.BrowseTheWeb.as(actor)
.navigateTo(url)
.catch(error => {
async performAs(actor) {
const url = await actor.answer(this.url);
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.navigateTo(url).catch(error => {
throw new core_1.TestCompromisedError(`Couldn't navigate to ${url}`, error);
}));
});
}

@@ -192,0 +200,0 @@ /**

@@ -94,3 +94,4 @@ "use strict";

const keys = await actor.answer(this.keys);
return abilities_1.BrowseTheWeb.as(actor).sendKeys(keys);
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.sendKeys(keys);
}

@@ -115,4 +116,5 @@ }

const keys = await actor.answer(this.keys);
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
// fix for protractor
await abilities_1.BrowseTheWeb.as(actor).executeScript(
await page.executeScript(
/* istanbul ignore next */

@@ -122,3 +124,3 @@ function focus(element) {

}, await field.nativeElement());
return abilities_1.BrowseTheWeb.as(actor).sendKeys(keys);
return page.sendKeys(keys);
}

@@ -125,0 +127,0 @@ }

@@ -45,3 +45,3 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

export declare class Scroll extends Interaction {
private readonly target;
private readonly element;
/**

@@ -58,6 +58,6 @@ * @desc

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be scroll to
*/
constructor(target: Answerable<PageElement>);
constructor(element: Answerable<PageElement>);
/**

@@ -64,0 +64,0 @@ * @desc

@@ -5,3 +5,2 @@ "use strict";

const core_1 = require("@serenity-js/core");
const io_1 = require("@serenity-js/core/lib/io");
/**

@@ -50,8 +49,8 @@ * @desc

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be scroll to
*/
constructor(target) {
constructor(element) {
super();
this.target = target;
this.element = element;
}

@@ -85,3 +84,3 @@ /**

async performAs(actor) {
const target = await actor.answer(this.target);
const target = await actor.answer(this.element);
return target.scrollIntoView();

@@ -96,3 +95,3 @@ }

toString() {
return (0, io_1.formatted) `#actor scrolls to ${this.target}`;
return (0, core_1.d) `#actor scrolls to ${this.element}`;
}

@@ -99,0 +98,0 @@ }

@@ -68,7 +68,6 @@ "use strict";

return {
from: (pageElement) => screenplay_1.Interaction.where((0, io_1.formatted) `#actor selects value ${value} from ${pageElement}`, async (actor) => {
return models_1.PageElement.located(models_1.By.css((0, core_1.q) `option[value=${value}]`))
.of(pageElement)
.click()
.performAs(actor);
from: (pageElement) => screenplay_1.Interaction.where((0, core_1.d) `#actor selects value ${value} from ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const desiredValue = await actor.answer(value);
await element.selectOptions(models_1.SelectOption.withValue(desiredValue));
}),

@@ -130,11 +129,4 @@ };

const desiredValues = answers.flat();
const options = await models_1.PageElements.located(models_1.By.css(`option`))
.of(pageElement)
.answeredBy(actor);
for (const option of options) {
const shouldSelect = await optionsToSelect(hasValueEqualOneOf(desiredValues))(option);
if (shouldSelect) {
await option.click();
}
}
const element = await actor.answer(pageElement);
await element.selectOptions(...desiredValues.map(value => models_1.SelectOption.withValue(value)));
}),

@@ -195,7 +187,6 @@ };

return {
from: (pageElement) => screenplay_1.Interaction.where((0, io_1.formatted) `#actor selects ${value} from ${pageElement}`, async (actor) => {
return models_1.PageElement.located(models_1.By.cssContainingText('option', value))
.of(pageElement)
.click()
.performAs(actor);
from: (pageElement) => screenplay_1.Interaction.where((0, core_1.d) `#actor selects ${value} from ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const desiredLabel = await actor.answer(value);
await element.selectOptions(models_1.SelectOption.withLabel(desiredLabel));
}),

@@ -258,10 +249,5 @@ };

const answers = await (0, io_1.asyncMap)(values, value => actor.answer(value));
const desiredOptions = answers.flat();
const options = await models_1.PageElements.located(models_1.By.css(`option`)).of(pageElement).answeredBy(actor);
for (const option of options) {
const shouldSelect = await optionsToSelect(hasTextEqualOneOf(desiredOptions))(option);
if (shouldSelect) {
await option.click();
}
}
const desiredLabels = answers.flat();
const element = await actor.answer(pageElement);
await element.selectOptions(...desiredLabels.map(label => models_1.SelectOption.withLabel(label)));
}),

@@ -272,29 +258,2 @@ };

exports.Select = Select;
/** @package */
function hasValueEqualOneOf(desiredValues) {
return async (option) => {
const value = await option.value();
return desiredValues.includes(value);
};
}
/** @package */
function hasTextEqualOneOf(desiredValues) {
return async (option) => {
const value = await option.text();
return desiredValues.includes(value);
};
}
/** @package */
function optionsToSelect(criterion) {
return (option) => isAlreadySelected(option)
.then(alreadySelected => criterion(option).then(criterionMet => xor(alreadySelected, criterionMet)));
}
/** @package */
function isAlreadySelected(option) {
return option.isSelected();
}
/** @package */
function xor(first, second) {
return first !== second;
}
//# sourceMappingURL=Select.js.map

@@ -70,3 +70,4 @@ "use strict";

async performAs(actor) {
const screenshot = await abilities_1.BrowseTheWeb.as(actor).takeScreenshot();
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
const screenshot = await page.takeScreenshot();
const name = await actor.answer(this.name);

@@ -73,0 +74,0 @@ actor.collect(model_1.Photo.fromBase64(screenshot), new model_1.Name(name));

import { Answerable, Interaction, Optional, QuestionAdapter, Timestamp } from '@serenity-js/core';
import { CookieData } from './CookieData';
/**
* @desc
* A Screenplay Pattern-style model responsible for managing cookies available to the {@link Page}s.
*
* @implements {@serenity-js/core/lib/screenplay~Optional}
*/
export declare abstract class Cookie implements Optional {

@@ -32,2 +38,8 @@ protected readonly cookieName: string;

protected constructor(cookieName: string);
/**
* @desc
* Returns the name of this cookie.
*
* @returns {string}
*/
name(): string;

@@ -34,0 +46,0 @@ /**

@@ -9,2 +9,8 @@ "use strict";

const abilities_1 = require("../abilities");
/**
* @desc
* A Screenplay Pattern-style model responsible for managing cookies available to the {@link Page}s.
*
* @implements {@serenity-js/core/lib/screenplay~Optional}
*/
class Cookie {

@@ -25,3 +31,4 @@ constructor(cookieName) {

const cookieName = await actor.answer(name);
return abilities_1.BrowseTheWeb.as(actor).cookie(cookieName);
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.cookie(cookieName);
});

@@ -41,2 +48,3 @@ }

const cookie = (0, tiny_types_1.ensure)('cookieData', await actor.answer(cookieData), (0, tiny_types_1.isDefined)(), (0, tiny_types_1.isPlainObject)());
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
const sanitisedCookieData = {

@@ -52,3 +60,3 @@ name: (0, tiny_types_1.ensure)(`Cookie.set(cookieData.name)`, cookie.name, (0, tiny_types_1.isDefined)(), (0, tiny_types_1.isString)()),

};
return abilities_1.BrowseTheWeb.as(actor).setCookie(sanitisedCookieData);
return page.setCookie(sanitisedCookieData);
});

@@ -63,6 +71,13 @@ }

static deleteAll() {
return core_1.Interaction.where(`#actor deletes all cookies`, actor => {
return abilities_1.BrowseTheWeb.as(actor).deleteAllCookies();
return core_1.Interaction.where(`#actor deletes all cookies`, async (actor) => {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
await page.deleteAllCookies();
});
}
/**
* @desc
* Returns the name of this cookie.
*
* @returns {string}
*/
name() {

@@ -69,0 +84,0 @@ return this.cookieName;

@@ -0,10 +1,13 @@

export * from './BrowsingSession';
export * from './Cookie';
export * from './CookieData';
export * from './dialogs';
export * from './Locator';
export * from './ModalDialog';
export * from './Page';
export * from './PageElement';
export * from './PageElements';
export * from './RootLocator';
export * from './SelectOption';
export * from './selectors';
export * from './Switchable';
export * from './SwitchableOrigin';

@@ -17,9 +17,12 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./BrowsingSession"), exports);
__exportStar(require("./Cookie"), exports);
__exportStar(require("./CookieData"), exports);
__exportStar(require("./dialogs"), exports);
__exportStar(require("./Locator"), exports);
__exportStar(require("./ModalDialog"), exports);
__exportStar(require("./Page"), exports);
__exportStar(require("./PageElement"), exports);
__exportStar(require("./PageElements"), exports);
__exportStar(require("./RootLocator"), exports);
__exportStar(require("./SelectOption"), exports);
__exportStar(require("./selectors"), exports);

@@ -26,0 +29,0 @@ __exportStar(require("./Switchable"), exports);

import { PageElement } from './PageElement';
import { RootLocator } from './RootLocator';
import { Selector } from './selectors';
export declare abstract class Locator<Native_Element_Type, Native_Root_Element_Type = any, Selector_Type extends Selector = Selector> {
protected readonly parentRoot: () => Promise<Native_Root_Element_Type> | Native_Root_Element_Type;
protected readonly selector: Selector_Type;
protected readonly locateElement: (root: Native_Root_Element_Type) => Promise<Native_Element_Type> | Native_Element_Type;
protected readonly locateAllElements: (root: Native_Root_Element_Type) => Promise<Array<Native_Element_Type>> | Array<Native_Element_Type>;
constructor(parentRoot: () => Promise<Native_Root_Element_Type> | Native_Root_Element_Type, selector: Selector_Type, locateElement: (root: Native_Root_Element_Type) => Promise<Native_Element_Type> | Native_Element_Type, locateAllElements: (root: Native_Root_Element_Type) => Promise<Array<Native_Element_Type>> | Array<Native_Element_Type>);
nativeElement(): Promise<Native_Element_Type>;
abstract of(parent: Locator<Native_Element_Type, Native_Root_Element_Type>): Locator<Native_Element_Type, Native_Root_Element_Type>;
export declare abstract class Locator<Native_Element_Type, Native_Selector_Type = any> extends RootLocator<Native_Element_Type> {
protected readonly parent: RootLocator<Native_Element_Type>;
readonly selector: Selector;
protected constructor(parent: RootLocator<Native_Element_Type>, selector: Selector);
abstract nativeElement(): Promise<Native_Element_Type>;
abstract allNativeElements(): Promise<Array<Native_Element_Type>>;
switchToFrame(element: Native_Element_Type): Promise<void>;
switchToParentFrame(): Promise<void>;
switchToMainFrame(): Promise<void>;
protected abstract nativeSelector(): Native_Selector_Type;
abstract of(parent: RootLocator<Native_Element_Type>): Locator<Native_Element_Type>;
abstract locate(child: Locator<Native_Element_Type>): Locator<Native_Element_Type>;
abstract element(): PageElement<Native_Element_Type>;

@@ -12,0 +17,0 @@ abstract allElements(): Promise<Array<PageElement<Native_Element_Type>>>;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Locator = void 0;
class Locator {
constructor(parentRoot, selector, locateElement, locateAllElements) {
this.parentRoot = parentRoot;
const RootLocator_1 = require("./RootLocator");
class Locator extends RootLocator_1.RootLocator {
constructor(parent, selector) {
super();
this.parent = parent;
this.selector = selector;
this.locateElement = locateElement;
this.locateAllElements = locateAllElements;
}
async nativeElement() {
return this.locateElement(await this.parentRoot());
async switchToFrame(element) {
await this.parent.switchToFrame(element);
}
async switchToParentFrame() {
await this.parent.switchToParentFrame();
}
async switchToMainFrame() {
await this.parent.switchToMainFrame();
}
toString() {

@@ -15,0 +21,0 @@ return this.selector.toString();

/// <reference types="node" />
import { Expectation, Optional, QuestionAdapter } from '@serenity-js/core';
import { CorrelationId } from '@serenity-js/core/lib/model';
import { URL } from 'url';
import { Key } from '../../input';
import { BrowsingSession } from './BrowsingSession';
import { Cookie } from './Cookie';
import { CookieData } from './CookieData';
import { ModalDialogHandler } from './dialogs';
import { PageElement } from './PageElement';
import { PageElements } from './PageElements';
import { RootLocator } from './RootLocator';
import { Selector } from './selectors';
import { Switchable } from './Switchable';
import { SwitchableOrigin } from './SwitchableOrigin';
export declare abstract class Page implements Optional, Switchable {
protected readonly handle: string;
export declare abstract class Page<Native_Element_Type = any> implements Optional, Switchable {
protected readonly session: BrowsingSession<Page<Native_Element_Type>>;
protected readonly rootLocator: RootLocator<Native_Element_Type>;
protected modalDialogHandler: ModalDialogHandler;
readonly id: CorrelationId;
static current(): QuestionAdapter<Page>;

@@ -13,5 +26,209 @@ static whichName(expectation: Expectation<string>): QuestionAdapter<Page>;

private static findMatchingPage;
constructor(handle: string);
constructor(session: BrowsingSession<Page<Native_Element_Type>>, rootLocator: RootLocator<Native_Element_Type>, modalDialogHandler: ModalDialogHandler, id: CorrelationId);
/**
* @desc
* Creates a {@link PageElement}, located by `selector`.
*
* @param {Selector} selector
* @returns {PageElement<Native_Element_Type>}
*/
abstract locate(selector: Selector): PageElement<Native_Element_Type>;
/**
* @desc
* Creates {@link PageElements}, located by `selector`.
*
* @param {Selector} selector
* @returns {PageElements<Native_Element_Type>}
*/
abstract locateAll(selector: Selector): PageElements<Native_Element_Type>;
/**
* @desc
* Navigate to a given destination, specified as an absolute URL
* or a path relative to any base URL configured in your web integration tool.
*
* @param {string} destination
* @returns {Promise<void>}
*
* @see https://webdriver.io/docs/options/#baseurl
* @see https://playwright.dev/docs/api/class-browser#browser-new-context
* @see https://playwright.dev/docs/api/class-testoptions#test-options-base-url
* @see https://github.com/angular/protractor/blob/master/lib/config.ts
*/
abstract navigateTo(destination: string): Promise<void>;
/**
* @desc
* Causes the browser to traverse one step backward in the joint session history
* of the current {@link Page} (the current top-level browsing context).
*
* This is equivalent to pressing the back button in the browser UI,
* or calling {@link window.history.back}
*
* @returns {Promise<void>}
*/
abstract navigateBack(): Promise<void>;
/**
* @desc
* Causes the browser to traverse one step forward in the joint session history
* of the current {@link Page} (the current top-level browsing context).
*
* This is equivalent to pressing the back button in the browser UI,
* or calling {@link window.history.forward}
*
* @returns {Promise<void>}
*/
abstract navigateForward(): Promise<void>;
/**
* @desc
* causes the browser to reload the {@link Page} in current top-level browsing context.
*
* @returns {Promise<void>}
*/
abstract reload(): Promise<void>;
/**
* @desc
* Send a sequence of {@link Key} strokes to the active element.
*
* @param {Array<Key | string>} keys
* Keys to enter
*
* @returns {Promise<void>}
*/
abstract sendKeys(keys: Array<Key | string>): Promise<void>;
/**
* @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 <caption>Perform a sleep in the browser under test</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* return arguments[0].tagName;
* `, Target.the('header').located(by.css(h1))
*
* @see https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeScript-java.lang.String-java.lang.Object...-
*
* @param {string | Function} script
* @param {any[]} args
*
* @returns {Promise<any>}
*
* @see {@link BrowseTheWeb#getLastScriptExecutionResult}
*/
abstract executeScript<Result, InnerArguments extends any[]>(script: string | ((...parameters: InnerArguments) => Result), ...args: InnerArguments): Promise<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 <caption>Perform a sleep in the browser under test</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* var delay = arguments[0];
* var callback = arguments[arguments.length - 1];
*
* window.setTimeout(callback, delay);
* `, 500)
*
* @example <caption>Return a value asynchronously</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* var callback = arguments[arguments.length - 1];
*
* callback('some return value')
* `).then(value => doSomethingWithThe(value))
*
* @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
*
* @returns {Promise<any>}
*
* @see {@link BrowseTheWeb#getLastScriptExecutionResult}
*/
abstract executeAsyncScript<Result, Parameters extends any[]>(script: string | ((...args: [...parameters: Parameters, callback: (result: Result) => void]) => void), ...args: Parameters): Promise<Result>;
/**
* @desc
* Returns the last result of calling {@link BrowseTheWeb#executeAsyncScript}
* or {@link BrowseTheWeb#executeScript}
*
* @returns {any}
*/
abstract lastScriptExecutionResult<R = any>(): R;
/**
* @desc
* Take a screenshot of the top-level browsing context's viewport.
*
* @return {Promise<string>}
* A promise that will resolve to a base64-encoded screenshot PNG
*/
abstract takeScreenshot(): Promise<string>;
/**
* @desc
* Retrieves a cookie identified by `name` and visible to this {@link Page}.
*
* @param {string} name
* @returns {Promise<Cookie>}
*/
abstract cookie(name: string): Promise<Cookie>;
/**
* @desc
* Adds a single cookie with {@link CookieData} to the cookie store associated
* with the active {@link Page}'s address.
*
* @param {CookieData} cookieData
* @returns {Promise<void>}
*/
abstract setCookie(cookieData: CookieData): Promise<void>;
/**
* @desc
* Removes all the cookies.
*
* @returns {Promise<void>}
*/
abstract deleteAllCookies(): Promise<void>;
/**
* @desc
* Retrieves the document title of the current top-level browsing context, equivalent to calling `document.title`.

@@ -66,3 +283,3 @@ *

* @desc
* Switches the current browsing context to the given pare
* Switches the current browsing context to the given page
* and returns an object that allows the caller to switch back

@@ -72,4 +289,7 @@ * to the previous context if needed.

* @returns {Promise<SwitchableOrigin>}
*
* @see {@link Switch}
* @see {@link Switchable}
*/
abstract switchTo(): Promise<SwitchableOrigin>;
switchTo(): Promise<SwitchableOrigin>;
/**

@@ -89,3 +309,16 @@ * @desc

abstract closeOthers(): Promise<void>;
/**
* @desc
* Returns the {@link ModalDialogHandler} for the current {@link Page}.
*
* @returns {ModalDialogHandler}
*/
modalDialog(): ModalDialogHandler;
/**
* @desc
* Returns a description of this object.
*
* @returns {string}
*/
toString(): string;
}

@@ -5,6 +5,14 @@ "use strict";

const core_1 = require("@serenity-js/core");
const tiny_types_1 = require("tiny-types");
const abilities_1 = require("../abilities");
class Page {
constructor(handle) {
this.handle = handle;
constructor(session, rootLocator, modalDialogHandler, id) {
this.session = session;
this.rootLocator = rootLocator;
this.modalDialogHandler = modalDialogHandler;
this.id = id;
(0, tiny_types_1.ensure)('session', session, (0, tiny_types_1.isDefined)());
(0, tiny_types_1.ensure)('rootLocator', rootLocator, (0, tiny_types_1.isDefined)());
(0, tiny_types_1.ensure)('modalDialogHandler', modalDialogHandler, (0, tiny_types_1.isDefined)());
(0, tiny_types_1.ensure)('id', id, (0, tiny_types_1.isDefined)());
}

@@ -43,4 +51,39 @@ static current() {

}
/**
* @desc
* Switches the current browsing context to the given page
* and returns an object that allows the caller to switch back
* to the previous context if needed.
*
* @returns {Promise<SwitchableOrigin>}
*
* @see {@link Switch}
* @see {@link Switchable}
*/
async switchTo() {
const originalPage = await this.session.currentPage();
await this.session.changeCurrentPageTo(this);
return {
switchBack: async () => {
await this.session.changeCurrentPageTo(originalPage);
}
};
}
/**
* @desc
* Returns the {@link ModalDialogHandler} for the current {@link Page}.
*
* @returns {ModalDialogHandler}
*/
modalDialog() {
return this.modalDialogHandler;
}
/**
* @desc
* Returns a description of this object.
*
* @returns {string}
*/
toString() {
return `page (handle=${this.handle})`;
return `page (id=${this.id.value})`;
}

@@ -47,0 +90,0 @@ }

import { Answerable, Optional, QuestionAdapter } from '@serenity-js/core';
import { Locator } from './Locator';
import { SelectOption } from './SelectOption';
import { Selector } from './selectors';

@@ -21,2 +22,4 @@ import { Switchable } from './Switchable';

abstract rightClick(): Promise<void>;
abstract selectOptions(...options: Array<SelectOption>): Promise<void>;
abstract selectedOptions(): Promise<Array<SelectOption>>;
abstract attribute(name: string): Promise<string>;

@@ -23,0 +26,0 @@ abstract text(): Promise<string>;

@@ -15,3 +15,4 @@ "use strict";

const bySelector = await actor.answer(selector);
return abilities_1.BrowseTheWeb.as(actor).locate(bySelector).element();
const currentPage = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return currentPage.locate(bySelector);
});

@@ -18,0 +19,0 @@ }

@@ -1,2 +0,2 @@

import { Answerable, List, MetaQuestion, Question } from '@serenity-js/core';
import { Answerable, List, MetaQuestion } from '@serenity-js/core';
import { Locator } from './Locator';

@@ -6,3 +6,3 @@ import { PageElement } from './PageElement';

export declare class PageElements<Native_Element_Type = any> extends List<PageElement<Native_Element_Type>> implements MetaQuestion<Answerable<PageElement<Native_Element_Type>>, Promise<Array<PageElement<Native_Element_Type>>>> {
protected readonly locator: Question<Promise<Locator<Native_Element_Type>>>;
protected readonly locator: Answerable<Locator<Native_Element_Type>>;
static located<NET>(selector: Answerable<Selector>): PageElements<NET>;

@@ -12,4 +12,4 @@ /**

*/
constructor(locator: Question<Promise<Locator<Native_Element_Type>>>);
constructor(locator: Answerable<Locator<Native_Element_Type>>);
of(parent: Answerable<PageElement<Native_Element_Type>>): PageElements<Native_Element_Type>;
}

@@ -27,5 +27,6 @@ "use strict";

function relativeToDocumentRoot(selector) {
return core_1.Question.about(selector.toString(), async (actor) => {
return core_1.Question.about(String(selector), async (actor) => {
const bySelector = await actor.answer(selector);
return abilities_1.BrowseTheWeb.as(actor).locate(bySelector);
const currentPage = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return currentPage.locate(bySelector).locator;
});

@@ -47,3 +48,3 @@ }

function allElementsOf(locator) {
return core_1.Question.about(`page elements located ${locator.toString()}`, async (actor) => {
return core_1.Question.about(`page elements located ${String(locator)}`, async (actor) => {
const resolved = await actor.answer(locator);

@@ -50,0 +51,0 @@ return resolved.allElements();

@@ -9,2 +9,3 @@ import { Answerable, Question } from '@serenity-js/core';

static css(selector: Answerable<string>): Question<Promise<ByCss>>;
static deepCss(selector: Answerable<string>): Question<Promise<ByCss>>;
static cssContainingText(selector: Answerable<string>, text: Answerable<string>): Question<Promise<ByCssContainingText>>;

@@ -11,0 +12,0 @@ static id(selector: Answerable<string>): Question<Promise<ById>>;

@@ -7,2 +7,3 @@ "use strict";

const ByCssContainingText_1 = require("./ByCssContainingText");
const ByDeepCss_1 = require("./ByDeepCss");
const ById_1 = require("./ById");

@@ -18,2 +19,8 @@ const ByTagName_1 = require("./ByTagName");

}
static deepCss(selector) {
return core_1.Question.about((0, core_1.f) `by deep css (${selector})`, async (actor) => {
const bySelector = await actor.answer(selector);
return new ByDeepCss_1.ByDeepCss(bySelector);
});
}
static cssContainingText(selector, text) {

@@ -20,0 +27,0 @@ return core_1.Question.about((0, core_1.f) `by css (${selector}) containing text ${text}`, async (actor) => {

export * from './By';
export * from './ByCss';
export * from './ByCssContainingText';
export * from './ByDeepCss';
export * from './ById';

@@ -5,0 +6,0 @@ export * from './ByTagName';

@@ -20,2 +20,3 @@ "use strict";

__exportStar(require("./ByCssContainingText"), exports);
__exportStar(require("./ByDeepCss"), exports);
__exportStar(require("./ById"), exports);

@@ -22,0 +23,0 @@ __exportStar(require("./ByTagName"), exports);

@@ -18,3 +18,6 @@ "use strict";

static result() {
return core_1.Question.about(`last script execution result`, actor => abilities_1.BrowseTheWeb.as(actor).lastScriptExecutionResult());
return core_1.Question.about(`last script execution result`, async (actor) => {
const page = await abilities_1.BrowseTheWeb.as(actor).currentPage();
return page.lastScriptExecutionResult();
});
}

@@ -21,0 +24,0 @@ }

@@ -1,2 +0,2 @@

import { Answerable, List, Question } from '@serenity-js/core';
import { Answerable, QuestionAdapter } from '@serenity-js/core';
import { PageElement } from '../models';

@@ -52,3 +52,3 @@ /**

*/
static valueOf(pageElement: Answerable<PageElement>): Question<Promise<string>>;
static valueOf(pageElement: Answerable<PageElement>): QuestionAdapter<string>;
/**

@@ -95,3 +95,3 @@ * @desc

*/
static valuesOf(pageElement: Answerable<PageElement>): List<string>;
static valuesOf(pageElement: Answerable<PageElement>): QuestionAdapter<Array<string>>;
/**

@@ -141,3 +141,3 @@ * @desc

*/
static optionIn(pageElement: Answerable<PageElement>): Question<Promise<string>>;
static optionIn(pageElement: Answerable<PageElement>): QuestionAdapter<string>;
/**

@@ -187,3 +187,3 @@ * @desc

*/
static optionsIn(pageElement: Answerable<PageElement>): List<string>;
static optionsIn(pageElement: Answerable<PageElement>): QuestionAdapter<Array<string>>;
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Selected = void 0;
const io_1 = require("@serenity-js/core/lib/io");
const models_1 = require("../models");
const Text_1 = require("./Text");
const Value_1 = require("./Value");
const core_1 = require("@serenity-js/core");
/**

@@ -58,6 +55,9 @@ * @desc

static valueOf(pageElement) {
return models_1.PageElement.located(models_1.By.css('option:checked'))
.of(pageElement)
.value()
.describedAs((0, io_1.formatted) `value selected in ${pageElement}`);
return core_1.Question.about((0, core_1.d) `value selected in ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.value)[0];
});
}

@@ -106,6 +106,9 @@ /**

static valuesOf(pageElement) {
return models_1.PageElements.located(models_1.By.css('option:checked'))
.of(pageElement)
.eachMappedTo(Value_1.Value)
.describedAs((0, io_1.formatted) `values selected in ${pageElement}`);
return core_1.Question.about((0, core_1.d) `values selected in ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.value);
});
}

@@ -157,6 +160,9 @@ /**

static optionIn(pageElement) {
return models_1.PageElement.located(models_1.By.css('option:checked'))
.of(pageElement)
.text()
.describedAs((0, io_1.formatted) `option selected in ${pageElement}`);
return core_1.Question.about((0, core_1.d) `option selected in ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.label)[0];
});
}

@@ -208,6 +214,9 @@ /**

static optionsIn(pageElement) {
return models_1.PageElements.located(models_1.By.css('option:checked'))
.of(pageElement)
.eachMappedTo(Text_1.Text)
.describedAs((0, io_1.formatted) `options selected in ${pageElement}`);
return core_1.Question.about((0, core_1.d) `options selected in ${pageElement}`, async (actor) => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.label);
});
}

@@ -214,0 +223,0 @@ }

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PhotoTakingStrategy = void 0;
const core_1 = require("@serenity-js/core");
const events_1 = require("@serenity-js/core/lib/events");

@@ -41,15 +42,6 @@ const model_1 = require("@serenity-js/core/lib/model");

stage.announce(new events_1.AsyncOperationAttempted(new model_1.Description(`[Photographer:${this.constructor.name}] Taking screenshot of '${nameSuffix}'...`), id));
let dialogIsPresent;
try {
dialogIsPresent = await browseTheWeb.modalDialog().then(dialog => dialog.isPresent());
if (dialogIsPresent) {
return stage.announce(new events_1.AsyncOperationCompleted(new model_1.Description(`[${this.constructor.name}] Aborted taking screenshot of '${nameSuffix}' because of a modal dialog obstructing the view`), id));
}
}
catch (error) {
return stage.announce(new events_1.AsyncOperationFailed(error, id));
}
try {
const capabilities = await browseTheWeb.browserCapabilities();
const screenshot = await browseTheWeb.takeScreenshot();
const page = await browseTheWeb.currentPage();
const screenshot = await page.takeScreenshot();
const context = [capabilities.platformName, capabilities.browserName, capabilities.browserVersion], photoName = this.combinedNameFrom(...context, nameSuffix);

@@ -72,7 +64,4 @@ stage.announce(new events_1.ActivityRelatedArtifactGenerated(event.sceneId, event.activityId, photoName, model_1.Photo.fromBase64(screenshot)));

shouldIgnore(error) {
return error.name && (error.name === 'NoSuchSessionError' ||
(error.name === 'ProtocolError' && error.message.includes('Target closed')));
// todo: add SauceLabs
// [0-0] 2021-12-02T01:32:36.402Z ERROR webdriver: Request failed with status 404 due to no such window: no such window: target window already closed
// [0-0] from unknown error: web view not found
return error instanceof core_1.LogicError
|| (error.name && error.name === core_1.LogicError.name);
}

@@ -79,0 +68,0 @@ }

{
"name": "@serenity-js/web",
"version": "3.0.0-rc.19",
"version": "3.0.0-rc.20",
"description": "Serenity/JS Screenplay Pattern APIs for the Web",

@@ -50,4 +50,4 @@ "author": {

"dependencies": {
"@serenity-js/assertions": "3.0.0-rc.19",
"@serenity-js/core": "3.0.0-rc.19",
"@serenity-js/assertions": "3.0.0-rc.20",
"@serenity-js/core": "3.0.0-rc.20",
"tiny-types": "^1.18.4"

@@ -59,8 +59,8 @@ },

"@types/chai": "^4.3.1",
"@types/mocha": "^9.0.0",
"mocha": "^9.1.3",
"ts-node": "^10.7.0",
"typescript": "^4.7.2"
"@types/mocha": "^9.1.1",
"mocha": "^10.0.0",
"ts-node": "10.8.0",
"typescript": "^4.7.4"
},
"gitHead": "dda2a6eab593e2525aa67888f197e48f2e3ae148"
"gitHead": "105a84c6ab140c06e38d328274473a32b290080d"
}

@@ -11,3 +11,3 @@ # Serenity/JS

To learn more about Serenity/JS, check out the video below, read the [tutorial](https://serenity-js.org/handbook/thinking-in-serenity-js/index.html), review the [examples](https://github.com/serenity-js/serenity-js/tree/master/examples), and create your own test suite with [Serenity/JS template projects](https://github.com/serenity-js).
To learn more about Serenity/JS, check out the video below, read the [tutorial](https://serenity-js.org/handbook/thinking-in-serenity-js/index.html), review the [examples](https://github.com/serenity-js/serenity-js/tree/main/examples), and create your own test suite with [Serenity/JS template projects](https://github.com/serenity-js).

@@ -14,0 +14,0 @@ If you have any questions, join us on [Serenity/JS Community Chat](https://gitter.im/serenity-js/Lobby).

@@ -0,1 +1,3 @@

export * from './BrowserWindowClosedError';
export * from './CookieMissingError';
export * from './ModalDialogObstructsScreenshotError';

@@ -30,4 +30,4 @@ import { Answerable, AnswersQuestions, Expectation, ExpectationMet, ExpectationNotMet } from '@serenity-js/core';

* @example <caption>Using an expectation in a synchronisation statement</caption>
* import { actorCalled, Duration } from '@serenity-js/core';
* import { By, PageElement, Wait } from '@serenity-js/web';
* import { actorCalled, Duration, Wait } from '@serenity-js/core';
* import { By, PageElement } from '@serenity-js/web';
*

@@ -97,11 +97,7 @@ * const submitButton = () =>

try {
const result = await fn(pageElement);
const result = await fn(pageElement);
return result
? new ExpectationMet(this.toString(), undefined, pageElement)
: new ExpectationNotMet(this.toString(), undefined, pageElement);
} catch(error) {
return new ExpectationNotMet(`${ description } (${ error.message })`, undefined, pageElement);
}
return result
? new ExpectationMet(this.toString(), undefined, pageElement)
: new ExpectationNotMet(this.toString(), undefined, pageElement);
}

@@ -108,0 +104,0 @@ );

@@ -16,3 +16,3 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -19,0 +19,0 @@ // todo: isFocused?

@@ -24,3 +24,3 @@ import { and } from '@serenity-js/assertions';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -27,0 +27,0 @@ export function isClickable(): Expectation<PageElement> {

@@ -15,3 +15,3 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -18,0 +18,0 @@ export function isEnabled(): Expectation<PageElement> {

@@ -16,3 +16,3 @@ import { and, isPresent } from '@serenity-js/assertions';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -19,0 +19,0 @@ export function isSelected(): Expectation<PageElement> {

@@ -18,3 +18,3 @@ import { Expectation } from '@serenity-js/core';

* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/

@@ -21,0 +21,0 @@ export function isVisible(): Expectation<PageElement> {

@@ -0,5 +1,35 @@

/**
* @desc
* Basic meta-data about the browser used in the test.
*
* @public
*/
export interface BrowserCapabilities {
/**
* @desc
* Name of the operating system platform the test is executed on, like `darwin`, `linux` or `windows`.
*
* @type {string | undefined}
* @public
*/
platformName?: string;
/**
* @desc
* Name of the Web browser the test is executed in, like `chrome`, `firefox` or `safari`.
*
* @type {string | undefined}
* @public
*/
browserName?: string;
/**
* @desc
* Version number of the browser the test is executed in.
*
* @type {string | undefined}
* @public
*/
browserVersion?: string;
}

@@ -1,8 +0,7 @@

import { Ability, Duration, f, LogicError, UsesAbilities } from '@serenity-js/core';
import { Ability, UsesAbilities } from '@serenity-js/core';
import { Key } from '../../input';
import { Cookie, CookieData, Locator, ModalDialog, Page, Selector } from '../models';
import { BrowsingSession, Page } from '../models';
import { BrowserCapabilities } from './BrowserCapabilities';
export abstract class BrowseTheWeb<Native_Element_Type = any, Native_Root_Element_Type = unknown> implements Ability {
export abstract class BrowseTheWeb<Native_Element_Type = any> implements Ability {

@@ -18,48 +17,22 @@ /**

*/
static as<NET = any, NRET = unknown>(actor: UsesAbilities): BrowseTheWeb<NET, NRET> {
return actor.abilityTo(BrowseTheWeb) as BrowseTheWeb<NET, NRET>;
static as<NET = any>(actor: UsesAbilities): BrowseTheWeb<NET> {
return actor.abilityTo(BrowseTheWeb) as BrowseTheWeb<NET>;
}
protected constructor(
protected locators: Map<new (...args: unknown[]) => Selector, (selector: Selector) => Locator<Native_Element_Type, Native_Root_Element_Type>>
) {
/**
* @param {BrowsingSession<Page>} session
*
* @protected
*/
protected constructor(protected readonly session: BrowsingSession<Page<Native_Element_Type>>) {
}
abstract navigateTo(destination: string): Promise<void>;
abstract navigateBack(): Promise<void>;
abstract navigateForward(): Promise<void>;
abstract reloadPage(): Promise<void>;
abstract waitFor(duration: Duration): Promise<void>;
abstract waitUntil(condition: () => boolean | Promise<boolean>, timeout: Duration): Promise<void>;
locate(selector: Selector): Locator<Native_Element_Type, Native_Root_Element_Type> {
for (const [ type, locatorFactory ] of this.locators) {
if (selector instanceof type) {
return locatorFactory(selector);
}
}
throw new LogicError(f `${ selector } is not supported by ${ this.constructor.name }`);
}
/**
* @desc
* Returns basic meta-data about the browser associated with this ability.
*
* @returns {Promise<BrowserCapabilities>}
*/
abstract browserCapabilities(): Promise<BrowserCapabilities>;
abstract sendKeys(keys: Array<Key | string>): Promise<void>;
abstract executeScript<Result, InnerArguments extends any[]>(
script: string | ((...parameters: InnerArguments) => Result),
...args: InnerArguments
): Promise<Result>;
abstract executeAsyncScript<Result, Parameters extends any[]>(
script: string | ((...args: [ ...parameters: Parameters, callback: (result: Result) => void ]) => void),
...args: Parameters
): Promise<Result>;
abstract lastScriptExecutionResult<R = any>(): R;
abstract takeScreenshot(): Promise<string>;
/**

@@ -71,3 +44,5 @@ * @desc

*/
abstract currentPage(): Promise<Page>;
async currentPage(): Promise<Page<Native_Element_Type>> {
return this.session.currentPage();
}

@@ -81,10 +56,5 @@ /**

*/
abstract allPages(): Promise<Array<Page>>;
abstract cookie(name: string): Promise<Cookie>;
abstract setCookie(cookieData: CookieData): Promise<void>;
abstract deleteAllCookies(): Promise<void>;
abstract modalDialog(): Promise<ModalDialog>;
allPages(): Promise<Array<Page<Native_Element_Type>>> {
return this.session.allPages();
}
}

@@ -1,3 +0,2 @@

import { Answerable, AnswersQuestions, Interaction, LogicError, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { Answerable, AnswersQuestions, d, Interaction, LogicError, UsesAbilities } from '@serenity-js/core';

@@ -69,3 +68,3 @@ import { PageElement } from '../models';

constructor(private readonly field: Answerable<PageElement>) {
super(formatted `#actor clears the value of ${ field }`);
super(d`#actor clears the value of ${ field }`);
}

@@ -89,11 +88,25 @@

const element = await this.resolve(actor, this.field);
const value = await element.value();
if (value === null || value === undefined) {
await this.ensureElementCanBeCleared(element);
return element.clearValue();
}
private async ensureElementCanBeCleared(element: PageElement): Promise<void> {
let threwError: Error,
hasValueAttribute = false;
try {
const value = await element.value();
hasValueAttribute = value !== null && value !== undefined;
} catch (error) {
threwError = error;
}
if (! hasValueAttribute || threwError) {
throw new LogicError(
this.capitaliseFirstLetter(formatted `${ this.field } doesn't seem to have a 'value' attribute that could be cleared.`),
this.capitaliseFirstLetter(d`${ this.field } doesn't seem to have a 'value' attribute that could be cleared.`),
threwError
);
}
return element.clearValue();
}

@@ -100,0 +113,0 @@

@@ -84,4 +84,4 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

await element.scrollIntoView();
return element.click();
await element.click();
}
}

@@ -41,4 +41,4 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

* @example <caption>Double-clicking on an element</caption>
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text, Wait } from '@serenity-js/webdriverio';
* import { actorCalled, Wait } from '@serenity-js/core';
* import { BrowseTheWeb, DoubleClick, isVisible, Enter, Text } from '@serenity-js/webdriverio';
* import { Ensure, equals, not } from '@serenity-js/assertions';

@@ -45,0 +45,0 @@ *

@@ -249,4 +249,5 @@ import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, LogicError, UsesAbilities } from '@serenity-js/core';

protected executeAs(actor: UsesAbilities & AnswersQuestions, args: any[]): Promise<any> {
return BrowseTheWeb.as(actor).executeAsyncScript(this.script as unknown as any, ...args); // todo: fix types
protected async executeAs(actor: UsesAbilities & AnswersQuestions, args: any[]): Promise<any> {
const page = await BrowseTheWeb.as(actor).currentPage();
return page.executeAsyncScript(this.script as unknown as any, ...args); // todo: fix types
}

@@ -284,34 +285,35 @@

*/
performAs(actor: UsesAbilities & AnswersQuestions): Promise<any> {
return BrowseTheWeb.as(actor)
.executeAsyncScript(
/* istanbul ignore next */
function executeScriptFromUrl(sourceUrl: string, callback: (message?: string) => void) {
const alreadyLoadedScripts = Array.prototype.slice
.call(document.querySelectorAll('script'))
.map(script => script.src);
async performAs(actor: UsesAbilities & AnswersQuestions): Promise<any> {
const page = await BrowseTheWeb.as(actor).currentPage();
if (~ alreadyLoadedScripts.indexOf(sourceUrl)) {
return callback('Script from ' + sourceUrl + ' has already been loaded');
}
return page.executeAsyncScript(
/* istanbul ignore next */
function executeScriptFromUrl(sourceUrl: string, callback: (message?: string) => void) {
const alreadyLoadedScripts = Array.prototype.slice
.call(document.querySelectorAll('script'))
.map(script => script.src);
const script = document.createElement('script');
script.addEventListener('load', function() {
callback();
});
script.addEventListener('error', function () {
return callback('Couldn\'t load script from ' + sourceUrl);
});
if (~ alreadyLoadedScripts.indexOf(sourceUrl)) {
return callback('Script from ' + sourceUrl + ' has already been loaded');
}
script.src = sourceUrl;
script.async = true;
document.head.append(script);
},
this.sourceUrl
)
.then(errorMessage => {
if (errorMessage) {
throw new LogicError(errorMessage);
}
});
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);
},
this.sourceUrl
)
.then(errorMessage => {
if (errorMessage) {
throw new LogicError(errorMessage);
}
});
}

@@ -339,4 +341,5 @@

protected executeAs(actor: UsesAbilities & AnswersQuestions, args: any[]): Promise<any> {
return BrowseTheWeb.as(actor).executeScript(this.script as unknown as any, ...args); // todo fix type
protected async executeAs(actor: UsesAbilities & AnswersQuestions, args: any[]): Promise<any> {
const page = await BrowseTheWeb.as(actor).currentPage();
return page.executeScript(this.script as unknown as any, ...args); // todo fix type
}

@@ -343,0 +346,0 @@

@@ -65,7 +65,7 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be hovered over
*/
constructor(private readonly target: Answerable<PageElement>) {
super(formatted `#actor hovers the mouse over ${ target }`);
constructor(private readonly element: Answerable<PageElement>) {
super(formatted `#actor hovers the mouse over ${ element }`);
}

@@ -88,5 +88,5 @@

async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
const element = await this.resolve(actor, this.target);
const element = await this.resolve(actor, this.element);
return element.hoverOver();
}
}

@@ -16,2 +16,1 @@ export * from './Clear';

export * from './TakeScreenshot';
export * from './Wait';

@@ -93,5 +93,7 @@ import { Answerable, AnswersQuestions, Interaction, TestCompromisedError, UsesAbilities } from '@serenity-js/core';

static back(): Interaction {
return Interaction.where(`#actor navigates back in the browser history`, actor =>
BrowseTheWeb.as(actor).navigateBack(),
);
return Interaction.where(`#actor navigates back in the browser history`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage();
await page.navigateBack();
});
}

@@ -129,5 +131,7 @@

static forward(): Interaction {
return Interaction.where(`#actor navigates forward in the browser history`, actor =>
BrowseTheWeb.as(actor).navigateForward(),
);
return Interaction.where(`#actor navigates forward in the browser history`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage();
await page.navigateForward();
});
}

@@ -162,5 +166,7 @@

static reloadPage(): Interaction {
return Interaction.where(`#actor reloads the page`, actor =>
BrowseTheWeb.as(actor).reloadPage(),
);
return Interaction.where(`#actor reloads the page`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage();
await page.reload();
});
}

@@ -192,11 +198,9 @@ }

*/
performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return actor.answer(this.url)
.then(url =>
BrowseTheWeb.as(actor)
.navigateTo(url)
.catch(error => {
throw new TestCompromisedError(`Couldn't navigate to ${ url }`, error);
})
)
async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
const url = await actor.answer(this.url);
const page = await BrowseTheWeb.as(actor).currentPage();
return page.navigateTo(url).catch(error => {
throw new TestCompromisedError(`Couldn't navigate to ${ url }`, error);
});
}

@@ -203,0 +207,0 @@

@@ -99,4 +99,5 @@ import { Activity, Answerable, AnswersQuestions, Interaction, Question, UsesAbilities } from '@serenity-js/core';

async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
const keys = await actor.answer(this.keys);
return BrowseTheWeb.as(actor).sendKeys(keys);
const keys = await actor.answer(this.keys);
const page = await BrowseTheWeb.as(actor).currentPage();
return page.sendKeys(keys);
}

@@ -123,5 +124,6 @@ }

const keys = await actor.answer(this.keys);
const page = await BrowseTheWeb.as(actor).currentPage();
// fix for protractor
await BrowseTheWeb.as(actor).executeScript(
await page.executeScript(
/* istanbul ignore next */

@@ -134,3 +136,3 @@ function focus(element: any) {

return BrowseTheWeb.as(actor).sendKeys(keys);
return page.sendKeys(keys);
}

@@ -137,0 +139,0 @@ }

@@ -1,3 +0,2 @@

import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { Answerable, AnswersQuestions, d, Interaction, UsesAbilities } from '@serenity-js/core';

@@ -63,6 +62,6 @@ import { PageElement } from '../models';

/**
* @param {Answerable<PageElement>} target
* @param {Answerable<PageElement>} element
* The element to be scroll to
*/
constructor(private readonly target: Answerable<PageElement>) {
constructor(private readonly element: Answerable<PageElement>) {
super();

@@ -86,3 +85,3 @@ }

async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
const target = await actor.answer(this.target);
const target = await actor.answer(this.element);

@@ -99,4 +98,4 @@ return target.scrollIntoView();

toString(): string {
return formatted `#actor scrolls to ${ this.target }`;
return d`#actor scrolls to ${ this.element }`;
}
}

@@ -1,7 +0,7 @@

import { Answerable, q } from '@serenity-js/core';
import { asyncMap, commaSeparated, formatted } from '@serenity-js/core/lib/io';
import { Answerable, d } from '@serenity-js/core';
import { asyncMap, commaSeparated } from '@serenity-js/core/lib/io';
import { inspected } from '@serenity-js/core/lib/io/inspected';
import { Interaction } from '@serenity-js/core/lib/screenplay';
import { By, PageElement, PageElements } from '../models';
import { PageElement, SelectOption } from '../models';
import { SelectBuilder } from './SelectBuilder';

@@ -70,7 +70,7 @@

from: (pageElement: Answerable<PageElement>): Interaction =>
Interaction.where(formatted `#actor selects value ${ value } from ${ pageElement }`, async actor => {
return PageElement.located(By.css(q`option[value=${ value }]`))
.of(pageElement)
.click()
.performAs(actor);
Interaction.where(d`#actor selects value ${ value } from ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const desiredValue = await actor.answer(value);
await element.selectOptions(SelectOption.withValue(desiredValue));
}),

@@ -133,15 +133,8 @@ };

const answers = await asyncMap(values, value => actor.answer(value));
const answers = await asyncMap(values, value => actor.answer(value));
const desiredValues = answers.flat();
const options: PageElement[] = await PageElements.located(By.css(`option`))
.of(pageElement)
.answeredBy(actor);
const element = await actor.answer(pageElement);
for (const option of options) {
const shouldSelect = await optionsToSelect(hasValueEqualOneOf(desiredValues))(option);
if (shouldSelect) {
await option.click();
}
}
await element.selectOptions(... desiredValues.map(value => SelectOption.withValue(value)));
}),

@@ -204,7 +197,7 @@ };

from: (pageElement: Answerable<PageElement>): Interaction =>
Interaction.where(formatted `#actor selects ${ value } from ${ pageElement }`, async actor => {
return PageElement.located(By.cssContainingText('option', value))
.of(pageElement)
.click()
.performAs(actor);
Interaction.where(d`#actor selects ${ value } from ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const desiredLabel = await actor.answer(value);
await element.selectOptions(SelectOption.withLabel(desiredLabel));
}),

@@ -269,13 +262,8 @@ };

const answers = await asyncMap(values, value => actor.answer(value));
const desiredOptions = answers.flat();
const answers = await asyncMap(values, value => actor.answer(value));
const desiredLabels = answers.flat();
const options: PageElement[] = await PageElements.located(By.css(`option`)).of(pageElement).answeredBy(actor);
const element = await actor.answer(pageElement);
for (const option of options) {
const shouldSelect = await optionsToSelect(hasTextEqualOneOf(desiredOptions))(option);
if (shouldSelect) {
await option.click()
}
}
await element.selectOptions(... desiredLabels.map(label => SelectOption.withLabel(label)));
}),

@@ -285,42 +273,1 @@ };

}
/** @package */
function hasValueEqualOneOf(desiredValues: string[]): (option: PageElement) => Promise<boolean> {
return async (option: PageElement) => {
const value = await option.value()
return desiredValues.includes(value);
}
}
/** @package */
function hasTextEqualOneOf(desiredValues: string[]): (option: PageElement) => Promise<boolean> {
return async (option: PageElement) => {
const value = await option.text()
return desiredValues.includes(value);
}
}
/** @package */
function optionsToSelect(criterion: (option: PageElement) => Promise<boolean>) {
return (option: PageElement) =>
isAlreadySelected(option)
.then(alreadySelected =>
criterion(option).then(criterionMet =>
xor(alreadySelected, criterionMet)
)
);
}
/** @package */
function isAlreadySelected(option: PageElement): Promise<boolean> {
return option.isSelected();
}
/** @package */
function xor(first: boolean, second: boolean): boolean {
return first !== second;
}

@@ -71,3 +71,4 @@ import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, UsesAbilities } from '@serenity-js/core';

async performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> {
const screenshot = await BrowseTheWeb.as(actor).takeScreenshot();
const page = await BrowseTheWeb.as(actor).currentPage();
const screenshot = await page.takeScreenshot();
const name = await actor.answer(this.name);

@@ -74,0 +75,0 @@

@@ -9,2 +9,8 @@ import { Answerable, Interaction, Optional, Question, QuestionAdapter, Timestamp } from '@serenity-js/core';

/**
* @desc
* A Screenplay Pattern-style model responsible for managing cookies available to the {@link Page}s.
*
* @implements {@serenity-js/core/lib/screenplay~Optional}
*/
export abstract class Cookie implements Optional {

@@ -21,4 +27,5 @@

return Question.about(`"${ name }" cookie`, async actor => {
const cookieName = await actor.answer(name);
return BrowseTheWeb.as(actor).cookie(cookieName);
const cookieName = await actor.answer(name);
const page = await BrowseTheWeb.as(actor).currentPage();
return page.cookie(cookieName);
});

@@ -41,2 +48,4 @@ }

const page = await BrowseTheWeb.as(actor).currentPage();
const sanitisedCookieData: CookieData = {

@@ -53,3 +62,3 @@ name: ensure(`Cookie.set(cookieData.name)`, cookie.name, isDefined(), isString()),

return BrowseTheWeb.as(actor).setCookie(sanitisedCookieData);
return page.setCookie(sanitisedCookieData);
});

@@ -65,4 +74,5 @@ }

static deleteAll(): Interaction {
return Interaction.where(`#actor deletes all cookies`, actor => {
return BrowseTheWeb.as(actor).deleteAllCookies();
return Interaction.where(`#actor deletes all cookies`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage();
await page.deleteAllCookies();
});

@@ -77,2 +87,8 @@ }

/**
* @desc
* Returns the name of this cookie.
*
* @returns {string}
*/
name(): string {

@@ -79,0 +95,0 @@ return this.cookieName;

@@ -0,10 +1,13 @@

export * from './BrowsingSession';
export * from './Cookie';
export * from './CookieData';
export * from './dialogs';
export * from './Locator';
export * from './ModalDialog';
export * from './Page';
export * from './PageElement';
export * from './PageElements';
export * from './RootLocator';
export * from './SelectOption';
export * from './selectors';
export * from './Switchable';
export * from './SwitchableOrigin';
import { PageElement } from './PageElement';
import { RootLocator } from './RootLocator';
import { Selector } from './selectors';
export abstract class Locator<Native_Element_Type, Native_Root_Element_Type = any, Selector_Type extends Selector = Selector> {
constructor(
protected readonly parentRoot: () => Promise<Native_Root_Element_Type> | Native_Root_Element_Type,
protected readonly selector: Selector_Type,
protected readonly locateElement: (root: Native_Root_Element_Type) => Promise<Native_Element_Type> | Native_Element_Type,
protected readonly locateAllElements: (root: Native_Root_Element_Type) => Promise<Array<Native_Element_Type>> | Array<Native_Element_Type>,
export abstract class Locator<Native_Element_Type, Native_Selector_Type = any>
extends RootLocator<Native_Element_Type>
{
protected constructor(
protected readonly parent: RootLocator<Native_Element_Type>,
public readonly selector: Selector,
) {
super();
}
public async nativeElement(): Promise<Native_Element_Type> {
return this.locateElement(await this.parentRoot());
public abstract nativeElement(): Promise<Native_Element_Type>;
public abstract allNativeElements(): Promise<Array<Native_Element_Type>>;
async switchToFrame(element: Native_Element_Type): Promise<void> {
await this.parent.switchToFrame(element);
}
abstract of(parent: Locator<Native_Element_Type, Native_Root_Element_Type>): Locator<Native_Element_Type, Native_Root_Element_Type>;
async switchToParentFrame(): Promise<void> {
await this.parent.switchToParentFrame();
}
async switchToMainFrame(): Promise<void> {
await this.parent.switchToMainFrame();
}
protected abstract nativeSelector(): Native_Selector_Type;
abstract of(parent: RootLocator<Native_Element_Type>): Locator<Native_Element_Type>;
abstract locate(child: Locator<Native_Element_Type>): Locator<Native_Element_Type>;
abstract element(): PageElement<Native_Element_Type>;
abstract allElements(): Promise<Array<PageElement<Native_Element_Type>>>;

@@ -21,0 +38,0 @@

import { Expectation, ExpectationMet, ExpectationOutcome, LogicError, Optional, Question, QuestionAdapter } from '@serenity-js/core';
import { CorrelationId } from '@serenity-js/core/lib/model';
import { ensure, isDefined } from 'tiny-types';
import { URL } from 'url';
import { Key } from '../../input';
import { BrowseTheWeb } from '../abilities';
import { BrowsingSession } from './BrowsingSession';
import { Cookie } from './Cookie';
import { CookieData } from './CookieData';
import { ModalDialogHandler } from './dialogs';
import { PageElement } from './PageElement';
import { PageElements } from './PageElements';
import { RootLocator } from './RootLocator';
import { Selector } from './selectors';
import { Switchable } from './Switchable';
import { SwitchableOrigin } from './SwitchableOrigin';
export abstract class Page implements Optional, Switchable {
export abstract class Page<Native_Element_Type = any> implements Optional, Switchable {
static current(): QuestionAdapter<Page> {

@@ -66,4 +77,11 @@ return Question.about<Page>('current page', actor => {

constructor(
protected readonly handle: string,
protected readonly session: BrowsingSession<Page<Native_Element_Type>>,
protected readonly rootLocator: RootLocator<Native_Element_Type>,
protected modalDialogHandler: ModalDialogHandler,
public readonly id: CorrelationId,
) {
ensure('session', session, isDefined());
ensure('rootLocator', rootLocator, isDefined());
ensure('modalDialogHandler', modalDialogHandler, isDefined());
ensure('id', id, isDefined());
}

@@ -73,2 +91,226 @@

* @desc
* Creates a {@link PageElement}, located by `selector`.
*
* @param {Selector} selector
* @returns {PageElement<Native_Element_Type>}
*/
abstract locate(selector: Selector): PageElement<Native_Element_Type>;
/**
* @desc
* Creates {@link PageElements}, located by `selector`.
*
* @param {Selector} selector
* @returns {PageElements<Native_Element_Type>}
*/
abstract locateAll(selector: Selector): PageElements<Native_Element_Type>;
/**
* @desc
* Navigate to a given destination, specified as an absolute URL
* or a path relative to any base URL configured in your web integration tool.
*
* @param {string} destination
* @returns {Promise<void>}
*
* @see https://webdriver.io/docs/options/#baseurl
* @see https://playwright.dev/docs/api/class-browser#browser-new-context
* @see https://playwright.dev/docs/api/class-testoptions#test-options-base-url
* @see https://github.com/angular/protractor/blob/master/lib/config.ts
*/
abstract navigateTo(destination: string): Promise<void>;
/**
* @desc
* Causes the browser to traverse one step backward in the joint session history
* of the current {@link Page} (the current top-level browsing context).
*
* This is equivalent to pressing the back button in the browser UI,
* or calling {@link window.history.back}
*
* @returns {Promise<void>}
*/
abstract navigateBack(): Promise<void>;
/**
* @desc
* Causes the browser to traverse one step forward in the joint session history
* of the current {@link Page} (the current top-level browsing context).
*
* This is equivalent to pressing the back button in the browser UI,
* or calling {@link window.history.forward}
*
* @returns {Promise<void>}
*/
abstract navigateForward(): Promise<void>;
/**
* @desc
* causes the browser to reload the {@link Page} in current top-level browsing context.
*
* @returns {Promise<void>}
*/
abstract reload(): Promise<void>;
/**
* @desc
* Send a sequence of {@link Key} strokes to the active element.
*
* @param {Array<Key | string>} keys
* Keys to enter
*
* @returns {Promise<void>}
*/
abstract sendKeys(keys: Array<Key | string>): Promise<void>;
/**
* @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 <caption>Perform a sleep in the browser under test</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* return arguments[0].tagName;
* `, Target.the('header').located(by.css(h1))
*
* @see https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeScript-java.lang.String-java.lang.Object...-
*
* @param {string | Function} script
* @param {any[]} args
*
* @returns {Promise<any>}
*
* @see {@link BrowseTheWeb#getLastScriptExecutionResult}
*/
abstract executeScript<Result, InnerArguments extends any[]>(
script: string | ((...parameters: InnerArguments) => Result),
...args: InnerArguments
): Promise<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 <caption>Perform a sleep in the browser under test</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* var delay = arguments[0];
* var callback = arguments[arguments.length - 1];
*
* window.setTimeout(callback, delay);
* `, 500)
*
* @example <caption>Return a value asynchronously</caption>
* BrowseTheWeb.as(actor).executeAsyncScript(`
* var callback = arguments[arguments.length - 1];
*
* callback('some return value')
* `).then(value => doSomethingWithThe(value))
*
* @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
*
* @returns {Promise<any>}
*
* @see {@link BrowseTheWeb#getLastScriptExecutionResult}
*/
abstract executeAsyncScript<Result, Parameters extends any[]>(
script: string | ((...args: [ ...parameters: Parameters, callback: (result: Result) => void ]) => void),
...args: Parameters
): Promise<Result>;
/**
* @desc
* Returns the last result of calling {@link BrowseTheWeb#executeAsyncScript}
* or {@link BrowseTheWeb#executeScript}
*
* @returns {any}
*/
abstract lastScriptExecutionResult<R = any>(): R;
/**
* @desc
* Take a screenshot of the top-level browsing context's viewport.
*
* @return {Promise<string>}
* A promise that will resolve to a base64-encoded screenshot PNG
*/
abstract takeScreenshot(): Promise<string>;
/**
* @desc
* Retrieves a cookie identified by `name` and visible to this {@link Page}.
*
* @param {string} name
* @returns {Promise<Cookie>}
*/
abstract cookie(name: string): Promise<Cookie>;
/**
* @desc
* Adds a single cookie with {@link CookieData} to the cookie store associated
* with the active {@link Page}'s address.
*
* @param {CookieData} cookieData
* @returns {Promise<void>}
*/
abstract setCookie(cookieData: CookieData): Promise<void>;
/**
* @desc
* Removes all the cookies.
*
* @returns {Promise<void>}
*/
abstract deleteAllCookies(): Promise<void>;
/**
* @desc
* Retrieves the document title of the current top-level browsing context, equivalent to calling `document.title`.

@@ -123,3 +365,3 @@ *

* @desc
* Switches the current browsing context to the given pare
* Switches the current browsing context to the given page
* and returns an object that allows the caller to switch back

@@ -129,5 +371,19 @@ * to the previous context if needed.

* @returns {Promise<SwitchableOrigin>}
*
* @see {@link Switch}
* @see {@link Switchable}
*/
abstract switchTo(): Promise<SwitchableOrigin>;
async switchTo(): Promise<SwitchableOrigin> {
const originalPage = await this.session.currentPage();
await this.session.changeCurrentPageTo(this);
return {
switchBack: async (): Promise<void> => {
await this.session.changeCurrentPageTo(originalPage);
}
}
}
/**

@@ -149,5 +405,21 @@ * @desc

/**
* @desc
* Returns the {@link ModalDialogHandler} for the current {@link Page}.
*
* @returns {ModalDialogHandler}
*/
modalDialog(): ModalDialogHandler {
return this.modalDialogHandler;
}
/**
* @desc
* Returns a description of this object.
*
* @returns {string}
*/
toString(): string {
return `page (handle=${ this.handle })`;
return `page (id=${ this.id.value })`;
}
}

@@ -6,2 +6,3 @@ import { Answerable, d, Optional, Question, QuestionAdapter } from '@serenity-js/core';

import { Locator } from './Locator';
import { SelectOption } from './SelectOption';
import { Selector } from './selectors';

@@ -15,4 +16,6 @@ import { Switchable } from './Switchable';

return Question.about(d`page element located ${ selector }`, async actor => {
const bySelector = await actor.answer(selector);
return BrowseTheWeb.as<NET>(actor).locate(bySelector).element();
const bySelector = await actor.answer(selector);
const currentPage = await BrowseTheWeb.as<NET>(actor).currentPage();
return currentPage.locate(bySelector);
});

@@ -51,2 +54,4 @@ }

abstract rightClick(): Promise<void>; // todo: should this be a click() call with a parameter?
abstract selectOptions(...options: Array<SelectOption>): Promise<void>;
abstract selectedOptions(): Promise<Array<SelectOption>>;

@@ -53,0 +58,0 @@ abstract attribute(name: string): Promise<string>;

@@ -19,3 +19,3 @@ import { Answerable, List, MetaQuestion, Question } from '@serenity-js/core';

*/
constructor(protected readonly locator: Question<Promise<Locator<Native_Element_Type>>>) {
constructor(protected readonly locator: Answerable<Locator<Native_Element_Type>>) {
super(allElementsOf(locator));

@@ -34,5 +34,6 @@ }

function relativeToDocumentRoot<Native_Element_Type>(selector: Answerable<Selector>): Question<Promise<Locator<Native_Element_Type>>> {
return Question.about(selector.toString(), async actor => {
return Question.about(String(selector), async actor => {
const bySelector = await actor.answer(selector);
return BrowseTheWeb.as(actor).locate(bySelector);
const currentPage = await BrowseTheWeb.as(actor).currentPage();
return currentPage.locate(bySelector).locator;
});

@@ -56,4 +57,4 @@ }

*/
function allElementsOf<Native_Element_Type>(locator: Question<Promise<Locator<Native_Element_Type>>>): Question<Promise<Array<PageElement<Native_Element_Type>>>> {
return Question.about(`page elements located ${ locator.toString() }`, async actor => {
function allElementsOf<Native_Element_Type>(locator: Answerable<Locator<Native_Element_Type>>): Question<Promise<Array<PageElement<Native_Element_Type>>>> {
return Question.about(`page elements located ${ String(locator) }`, async actor => {
const resolved: Locator<Native_Element_Type> = await actor.answer(locator);

@@ -60,0 +61,0 @@ return resolved.allElements();

@@ -5,2 +5,3 @@ import { Answerable, f, Question } from '@serenity-js/core';

import { ByCssContainingText } from './ByCssContainingText';
import { ByDeepCss } from './ByDeepCss';
import { ById } from './ById';

@@ -18,2 +19,9 @@ import { ByTagName } from './ByTagName';

static deepCss(selector: Answerable<string>): Question<Promise<ByCss>> {
return Question.about(f`by deep css (${selector})`, async actor => {
const bySelector = await actor.answer(selector);
return new ByDeepCss(bySelector);
});
}
static cssContainingText(selector: Answerable<string>, text: Answerable<string>): Question<Promise<ByCssContainingText>> {

@@ -20,0 +28,0 @@ return Question.about(f`by css (${selector}) containing text ${ text }`, async actor => {

export * from './By';
export * from './ByCss';
export * from './ByCssContainingText';
export * from './ByDeepCss';
export * from './ById';

@@ -5,0 +6,0 @@ export * from './ByTagName';

@@ -18,5 +18,7 @@ import { Question, QuestionAdapter } from '@serenity-js/core';

static result<R>(): QuestionAdapter<R> {
return Question.about(`last script execution result`, actor =>
BrowseTheWeb.as(actor).lastScriptExecutionResult());
return Question.about(`last script execution result`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage();
return page.lastScriptExecutionResult<R>();
})
}
}

@@ -1,7 +0,4 @@

import { Answerable, List, Question } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { Answerable, d, Question, QuestionAdapter } from '@serenity-js/core';
import { By, PageElement, PageElements } from '../models';
import { Text } from './Text';
import { Value } from './Value';
import { PageElement } from '../models';

@@ -58,7 +55,11 @@ /**

*/
static valueOf(pageElement: Answerable<PageElement>): Question<Promise<string>> {
return PageElement.located(By.css('option:checked'))
.of(pageElement)
.value()
.describedAs(formatted `value selected in ${ pageElement }`);
static valueOf(pageElement: Answerable<PageElement>): QuestionAdapter<string> {
return Question.about(d`value selected in ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.value)[0];
});
}

@@ -107,7 +108,12 @@

*/
static valuesOf(pageElement: Answerable<PageElement>): List<string> {
return PageElements.located(By.css('option:checked'))
.of(pageElement)
.eachMappedTo(Value)
.describedAs(formatted `values selected in ${ pageElement }`);
static valuesOf(pageElement: Answerable<PageElement>): QuestionAdapter<Array<string>> {
return Question.about(d`values selected in ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.value);
});
}

@@ -159,7 +165,12 @@

*/
static optionIn(pageElement: Answerable<PageElement>): Question<Promise<string>> {
return PageElement.located(By.css('option:checked'))
.of(pageElement)
.text()
.describedAs(formatted `option selected in ${ pageElement }`);
static optionIn(pageElement: Answerable<PageElement>): QuestionAdapter<string> {
return Question.about(d`option selected in ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.label)[0];
});
}

@@ -211,8 +222,13 @@

*/
static optionsIn(pageElement: Answerable<PageElement>): List<string> {
return PageElements.located(By.css('option:checked'))
.of(pageElement)
.eachMappedTo(Text)
.describedAs(formatted `options selected in ${ pageElement }`);
static optionsIn(pageElement: Answerable<PageElement>): QuestionAdapter<Array<string>> {
return Question.about(d`options selected in ${ pageElement }`, async actor => {
const element = await actor.answer(pageElement);
const options = await element.selectedOptions();
return options
.filter(option => option.selected)
.map(option => option.label);
});
}
}

@@ -1,2 +0,2 @@

import { Stage } from '@serenity-js/core';
import { LogicError, Stage } from '@serenity-js/core';
import { ActivityFinished, ActivityRelatedArtifactGenerated, ActivityStarts, AsyncOperationAttempted, AsyncOperationCompleted, AsyncOperationFailed, DomainEvent } from '@serenity-js/core/lib/events';

@@ -51,20 +51,6 @@ import { CorrelationId, Description, Name, Photo } from '@serenity-js/core/lib/model';

let dialogIsPresent: boolean;
try {
dialogIsPresent = await browseTheWeb.modalDialog().then(dialog => dialog.isPresent());
if (dialogIsPresent) {
return stage.announce(new AsyncOperationCompleted(
new Description(`[${ this.constructor.name }] Aborted taking screenshot of '${ nameSuffix }' because of a modal dialog obstructing the view`),
id,
));
}
} catch (error) {
return stage.announce(new AsyncOperationFailed(error, id));
}
try {
const capabilities = await browseTheWeb.browserCapabilities();
const screenshot = await browseTheWeb.takeScreenshot();
const page = await browseTheWeb.currentPage();
const screenshot = await page.takeScreenshot();

@@ -109,10 +95,5 @@ const

private shouldIgnore(error: Error) {
return error.name && (
error.name === 'NoSuchSessionError' ||
(error.name === 'ProtocolError' && error.message.includes('Target closed'))
);
// todo: add SauceLabs
// [0-0] 2021-12-02T01:32:36.402Z ERROR webdriver: Request failed with status 404 due to no such window: no such window: target window already closed
// [0-0] from unknown error: web view not found
return error instanceof LogicError
|| (error.name && error.name === LogicError.name);
}
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc