Customizing an Element
There are usually three main reasons to create a custom PageElement class:
- Mapping a website component which is composed of smaller components
- Implementing the abstract
ValuePageElementclass for setting and getting values - Overwriting a base class method
Let's have a look at each of them!
Creating a "Composed" PageElement
Writing the Composed Element Class
Writing a "composed" PageElement only requires 3 steps:
- Extending the class
PageElement - Defining the interface for the
optsparameter of your class' constructor - Adding the child page nodes to your class
The FeedItem class located in the folder src/page_objects/page_elements/FeedItem.ts is a good example for a
"composed" PageElement. It consists of two child page nodes: a title and a description.
import { PageNodeStore } from '../stores';
import { IPageElementOpts, PageElement } from './PageElement';
export interface IFeedItemOpts<
Store extends PageNodeStore
> extends IPageElementOpts<Store> {}
export class FeedItem<
Store extends PageNodeStore
> extends PageElement<Store> {
constructor(selector: string, opts: IFeedItemOpts<Store>) {
super(selector, opts);
}
get title() {
return this.$.Element(
xpath('//div').classContains('Feed-itemName')
);
}
get description() {
return this.$.Element(
xpath('//div').classContains('Feed-itemDesc')
);
}
}
Please notice that defining an interface for your custom opts parameter, even though it does not differ from
IPageElementOpts, is a good practice because it makes future additions to the interface a lot easier.
Adding an Export Entry to Index.ts
Do not forget to add an export entry for your custom page element to the index.ts file
located in the src/page_objects/page_elements folder:
export * from './FeedItem';
We will need this export entry to import our class type and its opts interface from
a PageNodeStore.
Extending ValuePageElement
As soon as you want to test forms with wdio-workflo, you will need to create custom PageElement classes that can
set and retrieve values from HTML elements like <input>. For this scenario, wdio-workflo provides an abstract
ValuePageElement class, a child class of PageElement, that you can implement.
Let's take a look at the Input class located at src/page_objects/page_elements/Input.ts:
import { PageNodeStore } from '../stores';
import {
IValuePageElementOpts,
ValuePageElement,
ValuePageElementCurrently
} from './ValuePageElement';
export interface IInputOpts<
Store extends PageNodeStore
> extends IValuePageElementOpts<Store> {}
export class Input<
Store extends PageNodeStore
> extends ValuePageElement<Store, string> {
readonly currently: InputCurrently<Store, this> = new InputCurrently(this);
constructor(selector: string, opts: IInputOpts<Store>) {
super(selector, opts);
}
setValue(value: string): this {
this.element.setValue(value);
return this;
}
}
export class InputCurrently<
Store extends PageNodeStore,
PageElementType extends Input<Store>
> extends ValuePageElementCurrently<Store, PageElementType, string> {
getValue(): string {
return this.element.getValue();
}
}
Defining the type of value
If we extend ValuePageElement instead of PageElement, we need to provide a second type parameter as well:
the type of the value passed to the setValue method and retrieved by calling getValue().
For our Input class, we use string as the type of our value parameter.
Implementing setValue and getValue
Action Methods vs. State Retrieval Methods
Our super class ValuePageElement requires us to implement two methods: setValue and getValue.
Before we do so, we should remember that setValue and getValue fall into two different method categories:
setValueis an "action" methodgetValueis a "state retrieval" method
In our Guide about PageElements, we defined that "actions" should perform an implicit wait before the
actual interaction with the page takes place. They are always defined on the PageElement class itself.
"State retrieval" functions, on the other hand, can be defined on both the PageElement class itself or inside the
PageElementCurrently class. If defined on the PageElement class, "state retrieval" methods should also perform an implicit wait.
If defined on the PageElementCurrently class, they should return the current state of the application's GUI immediatly.
Luckily, we only need to implement getValue in our InputCurrently class because the Input.getValue method
internally invokes InputCurrently.getValue after performing an implicit wait.
Invoking the Implicit Wait
But where do we ever invoke the implicit wait in our Input.setValue implementation?
This happens automatically when calling this.element inside our Input class. Internally, this.element invokes
this._initialWait which waits for the wait condition (eg. visible) of PageElement to be met.
If we did not have to access this.element, we could have also invoked this._initialWait directly.
Be aware, though, that this.element only performs an implicit wait if called from inside a PageElement class.
If you call this.element from inside a PageElementCurrently class like InputCurrently.getValue does,
no implicit wait will be performed.
Setting the currently API accessor
We must not forget to set the .currently API accessor of our Input class. Otherwise, the implementation of
InputCurrently.getValue will not be available to Input.
For some strange reason, typescript (v3.4.5) is not able to infer the type of our currently class member when we assign an instance of InputCurrently to it. Therefore, we need to explicitly
define the types of currently, wait and eventually whenever we overwrite them:
export class Input<
Store extends PageNodeStore
> extends ValuePageElement<Store, string> {
// We need to explicitly set the type of `currently` to `InputCurrently`.
readonly currently: InputCurrently<Store, this> = new InputCurrently(this);
/*...*/
// If we forget to explicitly set the type of `currently`, TypeScript thinks it's `any`.
readonly currently = new InputCurrently(this);
}
When defining a PageElementCurrently (or a PageElementWait or PageElementEventually) class, we need to pass
a reference of the "parent" PageElement to the constructor because some methods of our currently/wait/eventually
APIs need to access properties of their "parent".
For this reason, PageElementCurrently, PageElementWait and PageElementEventually require an additional template type
parameter: the type of their "parent" PageElement. This PageElementType is the second template type parameter after the Store type.
Overwriting a Base Class Method
Component libraries for modern web development often do not stick to HTML standards.
A dropdown, for example, is often not implemented using a <select> and multiple <option> tags, but rather with a couple of <div>, <input> and <span> tags.
Selenium's and wdio-workflo's methods try to stick to HTML standards. If the web application
that you want to test does not, you might need to overwrite some methods of your base PageElement class.
Wdio-workflo's demo website, for example, is built using Microsoft's Office UI Fabric react component library. To indicate wether a checkbox is ticked, this library adds the CSS class "is-checked" to a <div>:

Wdio-workflo's PageElement base class already ships with an implementations of the
isChecked and not.isChecked methods. However, these implementations check if an
HTML attribute named checked is set on an HTML element:

If you take a look at the Checkbox class implemented in the file src/page_objects/page_elements/Checkbox.ts of the wdio-workflo-example repository,
you will notice that its CheckboxCurrently subclass therefore overwrites the default
implementations of isChecked and not.isChecked:
export class CheckboxCurrently<
Store extends PageNodeStore,
PageElementType extends Checkbox<Store>
> extends ValuePageElementCurrently<Store, PageElementType, boolean> {
getValue() {
return this.not.isChecked();
}
isChecked() {
return this.containsClass('is-checked');
}
get not() {
return {
...super.not,
isChecked: () => {
// we use an arrow function so that `this` is CheckboxCurrently instead of the `not` object
return this.not.containsClass('is-checked');
}
};
}
}
I think the code for the isChecked method is pretty self-explanatory. The not.isChecked requires us to overwrite a method inside the not object which requires a little more effort
for the first overwritten method. However, the steps are always the same:
- We declare a
notgetter in our class - We return an object including all method implementations of our base
notobject (...super.not) - We overwrite
notmethods using arrow functions, sothiswill be the class rather than thenotobject
Implementing new State Getter/Checker Methods
In our Checkbox example, we only needed to overwrite the isChecked and not.isChecked
implementations of the CheckboxCurrently subclass. Similar API functions like
wait.isChecked or eventually.isChecked will also work with our overwritten behavior
out-of-the-box because they internally invoke currently.isChecked.
If you want to add completely new state getter or checker methods, like hasType / containsType / hasAnyType to check
the HTML type attribute, you need to implement a couple more methods:
currently.hasType/currently.containsType/currently.hasAnyTypecurrently.not.hasType/currently.containsType/currently.hasAnyTypewait.hasTypewait.not.hasTypeeventually.hasTypeeventually.not.hasType- and maybe also a
currently.getTypeand a.getTypemethod on the page element class itself
You probably won't need to add completely new state getter or checker methods often,
therefore I will not add code examples in this guide. However, you can find these method implementations in the PageElement class located in the file src/page_objects/page_elements/PageElement.ts of the wdio-workflo-example repository.
For even more code examples for state getter and state checker methods,
take a look at the framework's core PageElement class implemented in the file src/page_objects/page_elements/PageElement.ts of the wdio-workflo repository.
Adding a Factory Method to a PageNodeStore
After creating a custom PageElement class, we have to add a factory method to a PageNodeStore so that our new
PageElement class can be instantiated properly.
A factory method always receives two parameters:
- The XPath selector for the
PageNode(Workflo.XPathis astringor anXPathBuilderinstance) - A subset of all properties of the
PageNode'soptsparameter which can be configured "publically"
The factory method for our Input class can be found inside the PageNodeStore class located at src/page_objects/stores/PageNodeStore.ts:
Input(
selector: Workflo.XPath,
opts?: Pick<IInputOpts<this>, Workflo.Store.BaseKeys>,
) {
return this._getElement<Input<this>, IInputOpts<this>>(
selector,
Input,
{
store: this,
...opts,
},
);
}
Picking public opts properties
In our example, IInputOpts has three properties (all inherited from IPageElementOpts):
- The
waitTypeused to determine if aPageElement's implicit waiting condition is met (eg.visible) - The default
timeoutused by all implicit and explicit wait methods of aPageElement store- an instance of aPageNodeStoreassociated with thePageElement
Usually, not all of these properties should be configurable publically.
For example, the store associated with the PageElement is always set by PageNodeStore internally.
Therefore, we can make use of TypeScript's Pick keyword to only select waitType and timeout as publically
configurable properties of the PageNode's opts. To save us some paperwork, these two very common properties are
stored in the type alias Workflo.Store.BaseKeys.
Creating an instance of PageElement
All that is left now is to create an instance of our Input class. PageNodeStore provides the following methods
for creating instances of our 4 basic PageNode classes:
_getElement_getList_getMap_getGroup
For our Input class, we use _getElement which has two template type parameters
- The type of our
PageElementclass (Input<this>) - The type of the
optsparameter of our class (IInputOpts<this>)
and three function parameters
- The XPath selector for our
PageElementclass - The class itself (which derives from
PageElement) - The "full"
optsparameter (merging the publically and internally configured properties)
Invoking the new Factory Method
That's it. We should now be able to invoke our new factory method to fetch an instance of our Input class:
import { stores } from `?/page_objects`;
stores.pageNode.Input(
xpath('//input')
)