To provide the developers the possibility to write more accessible page objects and tests, we provide Extensions.
An Extension is a small isolated reusable unit that can provide new functionality to page objects. Those reusable units allow developers to add, for example, click
functionality by reusing a default implementation provided by the framework.
We differentiate between Capabilities, Flows, and Models.
Capabilities provide GLSP-Client-specific functionality like accessing the command-palette
or popup
. Complex interaction possibilities with GLSP are defined there.
Flows define an action or sequence of actions the user would typically do, like clicking
, hovering
, or renaming
an element.
- The
Click
flow consists of only a single action, namely clicking on an element. - The
Rename
flow consists of actions like double-clicking on the element, writing the new name, and pressing enter.
Models are mainly used to provide semantics to page objects. The framework sometimes requires further information from the page objects to enable better usability. For example, the PLabelledElement
allows the page objects to define a label for an element. Afterward, elements based on those labels can be searched in the graph.
Capabilities and Flows provide default implementations most of the time; however, for Models, this is not always possible. Thus, making it necessary to use the interfaces directly.
The basis for Extensions are Mixins. Mixins allow us to define the class hierarchy for the page objects dynamically. Due to this reason, it is possible to reuse functionality as necessary without polluting the prototype chain and to only use the necessary functionality in the page objects.
const TaskManualMixin = Mix(PNode)
.flow(useClickableFlow)
.flow(useHoverableFlow)
.flow(useDeletableFlow)
.capability(useResizeHandleCapability)
.capability(usePopupCapability)
.capability(useCommandPaletteCapability)
.build();
The code builds the class hierarchy for a page object. It can be read as follows. The root class is of class PNode
. The class PNode
is extended with Clickable
, Hoverable
and Deletable
functionality. Finally, the capabilities ResizeHandleCapability
, PopupCapability
, and CommandPaletteCapability
are added.
The result of this chaining is a new base class with all the functionality (e.g., clicking
, deleting
, accessing the popup
) as listed. The TaskManualMixin
can be again the base class for any other mixin or used as the base class for a page object.
export class TaskManual extends TaskManualMixin implements PLabelledElement {...}
Models can not be always used similary to Capabilities and Flows. In this case, the page object needs to implement the PLabelledElement
interface directly and provide the necessary implementation. Afterward, the TaskManual
can be used in places where the PLabelledElement
is required.
Capabilities and Flows always consist of two necessary parts, namely the Extension-Declaration
and Extension-Provider
. Models have mostly only the Extension-Declaration
part. No default implementation is available in this case, and the developers must provide it themselves.
The Extension-Declaration
interface defines the functionality the Extension wants to provide.
export interface PopupCapability<TPopup extends Popup = Popup> {
popup(): TPopup;
popupText(): Promise<string>;
}
The Extension-Declaration
of the capability Popup
is defined in the interface PopupCapability
. It describes two methods, namely popup()
and popupText()
. The former returns the popup
page object, and the latter the popup's text directly.
export interface Clickable {
click(): Promise<void>;
dblclick(): Promise<void>;
}
The Extension-Declaration
of the flow Clickable
defines two methods that trigger different click actions.
The framework uses those interfaces, and the final implementation is open to the users. They can reuse default implementations or provide their custom implementations. This approach allows the developers to override, restructure, or extend the functionality when necessary.
The Extension-Provider
provides the default implementation for the specific Extension-Declaration
.
export function usePopupCapability<TBase extends ConstructorA<Locateable & Hoverable>>(Base: TBase): Capability<TBase, PopupCapability> {
abstract class Mixin extends Base implements PopupCapability {
popup(): Popup {
return new Popup(this);
}
async popupText(): Promise<string> {
await this.hover();
return this.popup().innerText();
}
}
return Mixin;
}
The Extension-Provider
is a function that returns the class implementing the interface of the Extension-Declaration
. The function requires a base class to allow correct prototype chaining. Constraining the possible base class (e.g., TBase extends ConstructorA<Locateable & Hoverable>
) is also possible. Only base classes that fulfill the specific constraint are allowed in this case. The implementation is up to the developers. The framework provides a default implementation.
Regardless, as the provider
and the declaration
is separated, it is possible to use completely new implementations without reusing the default providers
. It is only necessary to define a new function that returns a class implementing the Extension-Declaration
while respecting the class hierarchy and using it in the Mix.flow
or Mix.capability
methods.
Overriding only some aspects of a default provider
is also possible. As the providers always return a class, the returned class from the provider
could be used as the base class, as visible in the following snippet:
export function useCustomPopupCapability<TBase extends ConstructorA<Locateable & Hoverable>>(
Base: TBase
): Capability<TBase, PopupCapability> {
abstract class Mixin extends usePopupCapability(Base) implements PopupCapability {
override async popupText(): Promise<string> {
await this.hover();
return `Prefix: ${await this.popup().innerText()}`;
}
}
return Mixin;
}
const CustomTaskManualMixin = Mix(PNode)
.flow(useClickableFlow)
.flow(useHoverableFlow)
.flow(useDeletableFlow)
.capability(useResizeHandleCapability)
.capability(useCustomPopupCapability)
.capability(useCommandPaletteCapability)
.build();
// Or reuse
const CustomTaskManualMixin = Mix(TaskManualMixin).capability(useCustomPopupCapability).build();