@serenity-js/web
Version:
Serenity/JS Screenplay Pattern library offering a flexible, web driver-agnostic approach for interacting with web-based user interfaces and components, suitable for various testing contexts
262 lines (237 loc) • 10.5 kB
text/typescript
import type { Answerable, MetaQuestion, MetaQuestionAdapter, Optional , QuestionAdapter} from '@serenity-js/core';
import { Question, the } from '@serenity-js/core';
import { ensure, isDefined } from 'tiny-types';
import { BrowseTheWeb } from '../abilities';
import type { Locator } from './Locator';
import type { SelectOption } from './SelectOption';
import type { Selector } from './selectors';
import type { Switchable } from './Switchable';
import type { SwitchableOrigin } from './SwitchableOrigin';
/**
* Uses the [actor's](https://serenity-js.org/api/core/class/Actor/) [ability](https://serenity-js.org/api/core/class/Ability/) to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/) to identify
* a single Web element located by [`Selector`](https://serenity-js.org/api/web/class/Selector/).
*
* ## Learn more
* - [Page Element Query Language](https://serenity-js.org/handbook/web-testing/page-element-query-language)
* - [`Optional`](https://serenity-js.org/api/core/interface/Optional/)
* - [`Switchable`](https://serenity-js.org/api/web/interface/Switchable/)
*
* @group Models
*/
export abstract class PageElement<Native_Element_Type = any> implements Optional, Switchable {
static from<NET>(nativeElement: NET): MetaQuestionAdapter<PageElement<NET>, PageElement<NET>> {
return Question.about(`native page element`, async actor => {
const currentPage = await BrowseTheWeb.as<BrowseTheWeb<NET>>(actor).currentPage();
return currentPage.createPageElement(nativeElement);
});
}
static located<NET>(selector: Answerable<Selector>): MetaQuestionAdapter<PageElement<NET>, PageElement<NET>> {
return Question.about(the`page element located ${ selector }`, async actor => {
const bySelector = await actor.answer(selector);
const currentPage = await BrowseTheWeb.as<BrowseTheWeb<NET>>(actor).currentPage();
return currentPage.locate(bySelector);
});
}
static of<NET>(
childElement: MetaQuestionAdapter<PageElement<NET>, PageElement<NET>> | PageElement<NET>,
parentElement: Answerable<PageElement<NET>>
): MetaQuestionAdapter<PageElement<NET>, PageElement<NET>> {
return Question.about(the`${ childElement } of ${ parentElement }`, async actor => {
const parent = await actor.answer(parentElement);
const child = childElement.of(parent)
return actor.answer(child);
});
}
/**
* A static method producing a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that can be used with [`PageElements.eachMappedTo`](https://serenity-js.org/api/web/class/PageElements/#eachMappedTo) method
* to extract the HTML of each element in a collection.
*
* #### Example
*
* ```typescript
* import { actorCalled, Log } from '@serenity-js/core'
* import { Navigate, PageElement, By, Text } from '@serenity-js/web'
* import { includes } from '@serenity-js/assertions'
*
* await actorCalled('Debbie').attemptsTo(
* Navigate.to('https://serenity-js.org'),
*
* Log.the(
* PageElements.located(By.css('a'))
* .where(Text, includes('modular'))
* .eachMappedTo(PageElement.html())
* ),
* )
* ```
*/
static html<NET>(): MetaQuestion<PageElement<NET>, QuestionAdapter<string>> {
return {
of: (pageElement: Answerable<PageElement<NET>>) =>
Question.about(`outer HTML of ${pageElement}`, async actor => {
const element = await actor.answer(pageElement);
return element.html();
})
}
}
constructor(public readonly locator: Locator<Native_Element_Type>) {
ensure('native element locator', locator, isDefined());
}
/**
* Locates a child element that:
* - matches the given selector
* - is located within the `parentElement`
*
* @param parentElement
*/
abstract of(parentElement: PageElement<Native_Element_Type>): PageElement<Native_Element_Type>;
/**
* Traverses the element and its parents, heading toward the document root,
* until it finds a parent [`PageElement`](https://serenity-js.org/api/web/class/PageElement/) that matches its associated CSS selector.
*
* #### Example
*
* ```html
* <div class="form-entry">
* <input id="username" />
* <ul class="warnings">
* <li>Username should be an email address</li>
* </ul>
* </div>
* ```
*
* ```typescript
* class Username {
* static field = () =>
* PageElement.located(By.id('username'))
* .describedAs('username field')
*
* private static container = () =>
* PageElement.located(By.css('.form-entry'))
* .describedAs('form entry container')
*
* static warnings = () =>
* PageElements.located(By.css('ul.warnings li'))
* .describedAs('warnings')
* .of(
* Username.container().closestTo(Username.field())
* )
* }
* ```
*
* :::info
* This method relies on [Element: closest() API](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest),
* and so is only compatible with locating parent elements specified using the following CSS selectors:
* - [`ByCss`](https://serenity-js.org/api/web/class/ByCss/)
* - [`ById`](https://serenity-js.org/api/web/class/ById/)
* - [`ByTagName`](https://serenity-js.org/api/web/class/ByTagName/)
* :::
*
* @param childElement
* @returns
*
* #### Learn more
* - [Element: closest() method](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest)
*/
abstract closestTo(childElement: PageElement<Native_Element_Type>): PageElement<Native_Element_Type>;
/**
* An "escape hatch" providing access to the integration tool-specific implementation of a Web element.
*/
async nativeElement(): Promise<Native_Element_Type> {
return this.locator.nativeElement();
}
toString(): string {
return `PageElement located ${ this.locator.toString() }`;
}
abstract enterValue(value: string | number | Array<string | number>): Promise<void>;
abstract clearValue(): Promise<void>;
abstract click(): Promise<void>;
abstract doubleClick(): Promise<void>;
abstract scrollIntoView(): Promise<void>;
abstract hoverOver(): Promise<void>;
abstract rightClick(): Promise<void>;
abstract selectOptions(...options: Array<SelectOption>): Promise<void>;
abstract selectedOptions(): Promise<Array<SelectOption>>;
abstract attribute(name: string): Promise<string>;
abstract text(): Promise<string>;
abstract value(): Promise<string>;
/**
* An instance method that resolves to the value of the [`outerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML) property
* of the underlying element.
*
* #### Example
*
* ```typescript
* import { actorCalled, Log } from '@serenity-js/core'
* import { Navigate, PageElement, By } from '@serenity-js/web'
*
* await actorCalled('Debbie').attemptsTo(
* Navigate.to('https://serenity-js.org'),
*
* Log.the(
* PageElement.located(By.css('h1')).html()
* ),
* )
* ```
*/
abstract html(): Promise<string>;
/**
* When the element represents an [`iframe`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe),
* calling this method switches the current browsing context to the given `iframe` context.
*
* When used with other types of [Web `Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element),
* calling this method will have the same result as calling [`Element.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event).
*
* @returns
* Returns an object that allows the caller to switch back
* to the previous context if needed.
*
* #### Learn more
* - [`Switch`](https://serenity-js.org/api/web/class/Switch/)
* - [`Switchable`](https://serenity-js.org/api/web/interface/Switchable/)
*/
abstract switchTo(): Promise<SwitchableOrigin>;
/**
* Resolves to `true` when the underlying element [has focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus).
* Otherwise, resolves to `false`.
*/
abstract isActive(): Promise<boolean>;
/**
* Resolves to `true` when the underlying element can be clicked on.
* Otherwise, resolves to `false`.
*
* Please refer to test integration tool-specific documentation for details.
*/
abstract isClickable(): Promise<boolean>;
/**
* Resolves to `true` when the underlying
* element is not [explicitly disabled](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled)
*
* Please refer to test integration tool-specific documentation for details.
*/
abstract isEnabled(): Promise<boolean>;
/**
* Returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to `true` when the element
* is present in the [Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model),
* `false` otherwise.
*/
async isPresent(): Promise<boolean> {
return this.locator.isPresent();
}
/**
* Resolves to `true` when the underlying element:
* - has a [`selected` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option#attr-selected) for `<option />` elements
* - has a [`checked`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox) attribute for checkboxes
*
* Otherwise, resolves to `false`.
*/
abstract isSelected(): Promise<boolean>;
/**
* Resolves to `true` when the underlying element:
* - is not hidden, so doesn't have CSS style like `display: none`, `visibility: hidden` or `opacity: 0`
* - is within the browser viewport
* - doesn't have its centre covered by other elements
*
* Otherwise, resolves to `false`.
*/
abstract isVisible(): Promise<boolean>;
}