This section will not go over how to build and use a PageObject. For a beginner's guide, refer to our quick guide. This page will assume that the reader has gone through the quick guide.
PageLoader is based on the PageObject Design Pattern
and is built using Dart codegen. Users write the abstract PageObject
class and code generation implements this abstract PageObject
.
For example:
1. User writes `my_po.dart`.
2. PageLoader code generation generates `my_po.g.dart`.
Your PageObject will have two layers of wrapping:
- Your PageObject wraps a
PageLoaderElement
object. PageLoaderElement
wraps an element fromdart:html
orpackage:webdriver
.
The user is responsible for writing their own PageObject
and its
internal methods. In most cases, these user-defined methods will interact with the rich PageLoaderElement API
.
By itself, PageObjects
have no specific implementation until it is constructed with either a dart:html
Element or package:webdriver
WebDriver.
This allows for users to write a single PageObject
and use this in both HTML and Webdriver implementations.
Most of the methods within PageLoaderElement
behavior similarly in
both dart:html
and package:webdriver
implementations, but there are some cases where they may differ in usage and behavior. Refer to the PageLoaderElement API
for more information.
For every PageObject
code written, there is an equivalent generated code.
PageLoader uses the following dependencies to achieve code generation:
For example, if a user writes my_po.dart
, code generation step will
generate my_po.g.dart
.
Consider the following sample PageObject
:
// FILE: my_po.dart
import 'package:pageloader/pageloader.dart';
part 'my_po.g.dart';
@PageObject()
@CheckTag('my-special-tag')
abstract class MyPO {
MyPO();
factory MyPO.create(PageLoaderElement context) =
$MyPO.create;
@ByCss('red-text-box')
PageLoaderElement get _redTextBox;
@ByTagName('blue-button')
PageLoaderElement get _blueButton;
bool get blueButtonIsDisplayed => _blueButton
Future<void> clickBlueButton => _blueButton.click();
Future<void> clickAndTypeIntoRedBox(String text) async {
await _redTextBox.click();
await _redTextBox.type(text);
}
}
Let's break this down step-by-step to understand what is going on:
1. Part Clause
part 'my_po.g.dart';
// ...
Adding this part clause is necessary since my_po.g.dart
essentially
becomes an extension of my_po.dart
. This allows users to only import
my_po.dart
and get full access to the generated code rather than
having to import both files.
2. @PageObject()
and abstract class
When a user writes their own PageObject
, they are creating an
abstract class
and the code generation step
implements this.
@PageObject()
is used as the starting point for code generation step.
package:build
will identify all abstract classes annotated with this and
generate the implemented, non-abstract version of this class.
3. @CheckTag('my-special-tag')
Adding a @CheckTag(<tag-name>)
is optional, but
considered best practice. @CheckTag(...)
provides a runtime
check on whether the accessed element truly represents the
user's intended HTML Element tag.
From the above example PageObject
, suppose:
final myPo = MyPO.create(...);
await myPo.clickBlueButton();
The order of operations on myPo.clickBlueButton()
is:
- Get the root
PageLoaderElement
withinMyPo
. This is not guaranteed to represent'my-special-tag'
based on what was passed inMyPO.create(...)
. - Verify that this root element represents
'my-special-tag'
. - Execute
clickBlueButton()
using this element.
@CheckTag(...)
annotation is responsible for step 2; without this,
even if some other tag was passed, the test would not complain.
However, you may end up with hard to debug issues.
Let's consider a more realistic example as to why using
@CheckTag(...)
is a good idea. Suppose you have another PageObject
that wraps MyPO
:
// FILE: 'some_other_po.dart'
import 'package:pageloader/pageloader.dart';
import 'my_po.dart';
@CheckTag('some-other-tag')
@PageObject()
abstract class OtherPO {
OtherPO();
factory OtherPO.create(PageLoaderElement context) =
$OtherPO.create;
@ByClass('foo-class')
MyPO get myPO;
}
In this example, we have wrapped MyPO
within another PageObject
.
Note the finder @ByClass('some-class')
used to access MyPO
PageObject
: we are trying to get MyPO
by class, not tag name.
If we have the following html:
<some-other-tag>
<incorrect-tag class="foo-class">
</incorrect-tag>
</some-other-tag>
then we are getting the incorrect tag since we expected
'my-special-tag'
but instead got 'incorrect-tag'
. In this case,
@CheckTag('my-special-class')
will complain whenever myPO
is used
since it is being bound to the incorrect tag.
4. Constructors
Every PageObject
must have the following boilerplate constructors:
MyPO();
factory MyPO.create(PageLoaderElement context) =
$MyPO.create;
Note here that factory MyPO.create(...) = $MyPO.create;
is a
factory constructor that delegates the construction of MyPO
to its generated counterpart $MyPO
(from my_po.g.dart
).
5. PageLoader abstract getter method annotations
In the example:
@ByCss('red-text-box')
PageLoaderElement get _redTextBox;
@ByTagName('blue-button')
PageLoaderElement get _blueButton;
These are abstract getter methods that PageLoader hooks into during code generation.
Annotations used on these abstract getter methods come in two flavors:
- Finders (required, unique)
- Filters (optional, multiple)
Any abstract getter method requires exactly one Finder annotation in order for PageLoader to do its magic. In addition, each abstract getter method with a Finder annotation can have zero or more Filters. (However, typically you only need at most one.)
Available Finders are:
There is also another annotation that can be used in place of a
Finder annotation - the @root
annotation.
The @root
annotation provides the PageObject
direct access to its
currently bound PageLoaderElement
. For example:
@PageObject()
@CheckTag('root-tag')
abstract class MyPO {
// ...constructors...
@root
PageLoaderElement get _rootElement;
@ByTagName('child-tag')
PageLoaderElement get childElement;
}
If MyPO
is used to bind to root-tag
in the following:
<root-tag>
<child-tag></child-tag>
</root-tag>
Then _rootElement
is bound to the PageLoaderElement
representing
<root-tag>
and childElement
is bound to the PageLoaderElement
representing <child-tag>
.`
There are also two context finders that wrap around the 7 Finders (non-@root
) above:
Example of context Finders:
@First(@ByTagName('foo-tag'))
PageLoaderElement get firstFooTag;
@Global(@ByTagName('foo-tag'))
PageLoaderElement get globalFooTag;
Filters are useful if you want to filter the results obtained from the Finder annotation.
For example:
<div>
<my-item class="foo"></my-item>
<my-item class="bar"></my-item>
<my-item class="foo"></my-item>
<my-item class="bar"></my-item>
</div>
Then:
@ByTagName('my-item')
@WithClass('foo')
List<MyItemPO> get fooItems;
@ByTagName('my-item')
@WithClass('bar')
List<MyItemPO> get barItems;
@WithClass(...)
filters both of these getters such that
they only return PageObjects
with the filtered class.
Full list of Filters:
@IsDisplayed()
@IsTag(...)
@WithAttribute(...)
@WithClass(...)
@WithInnerText(...)
@WithProperty(...)
@WithVisibleText(...)
Finally, these annotations may only be used on abstract getter methods that return instances of type:
PageLoaderElement
PageObject
(class with@PageObject()
annotation)List<T>
where T is the above 2.
6. Lazy loading, existence, and the NullPageLoaderElement
Whenever any PageLoader entity (PageObject
or PageLoaderElement
) is
accessed, PageLoader will lazily access these elements.
For example:
<div>
<may-exist></may-exist>
</div>
PageObject:
@PageObject()
abstract class MyPO {
// ...constructors...
@ByTagName('may-exist')
PageLoaderElement get _mayExist;
bool get mayExist => _mayExist.exists;
String get mayExistInnerText => _mayExist.innerText;
}
Let's suppose that the <may-exist>
tag may or may not be currently
rendered in the HTML document. Certain behaviors (ex: clicking a button)
may remove this tag or add it back in. PageLoader is not able to
ascertain its existence until this element is accessed. Also, PageLoader
does not attempt to cache knowledge about its existence.
As a result, best practices recommends users to manually check every element (if it may not exist) before utilizing it to avoid runtime exceptions from being thrown:
String get mayExistInnerText =>
mayExist ? _mayExist.innerText : '';
There may be cases where you may want to return a null
value. However,
instead of returning null
, users should return an instance of
a NullPageLoaderElement
via @nullElement
annotation.
For example:
@PageObject()
abstract class MyShoppingListPO {
// ...constructors...
@ByTagName('shopping-list-item')
List<ShoppingListItemPO> get _listItems;
ShoppingListItemPO shoppingItemWithName(String itemName) =>
_listItems.firstWhere(
(item) => item.attributes.contains(itemName),
orElse: () => null);
}
The above is not recommended since it returns a null
value when
the method signature expects a ShoppingListItemPO
.
This means the user must do a null check in their test while also doing an existence check:
final shoppingList = MyShoppingListPO.create(...);
// Do stuff
final handbag = shoppingList.shoppingItemWithName('handbag');
if (handbag != null && handbag.exists) {
// Do something with `handbag`
}
Instead, the user should return a NullPageLoaderElement
-wrapped
ShoppingListItemPO
using @nullElement
annotation:
@PageObject()
abstract class MyShoppingListPO {
// ...constructors...
@ByTagName('shopping-list-item')
List<ShoppingListItemPO> get _listItems;
@nullElement
ShoppingListItemPO get _nullShoppingListItem;
ShoppingListItemPO shoppingItemWithName(String itemName) =>
_listItems.firstWhere(
(item) => item.attributes.contains(itemName),
orElse: () => _nullShoppingListItem);
}
Then, you can simplify the conditional checks to:
final shoppingList = MyShoppingListPO.create(...);
// Do stuff
final handbag = shoppingList.shoppingItemWithName('handbag');
if (handbag.exists) {
// Do something with `handbag`
}
7. Mixins
By design, PageLoader follows "composition over inheritance" structure.
Direct inheritance is not possible with PageLoader since every
PageObject
has a generated class that extends on its abstract class.
For example: class $MyPO extends MyPO {...}
with $MyPO
being the
generated version of MyPO
(user-written).
However, we can use Dart mixins to provide shared functionality
between PageObjects
.
Example:
@PageObject()
mixin SharedPOMixin {
// Note: Do NOT add constructors
@ByTagName('foobar')
PageLoaderElement get foobarElement;
Future<void> clickFoobar() => foobarElement.click();
}
Then:
@PageObject()
abstract class ConcretePO with SharedPOMixin {
ConcretePO();
factory ConcretePO.create(PageLoaderElement context) =
$ConcretePO.create;
Future<void> clickFoobarAndReturnText() {
await clickFoobar();
return foobarElement.innerText;
}
}
You can also achieve even more advanced mixin combinations:
@PageObject()
mixin EmployeePOMixin {...}
// `TeacherPOMixin` cannot be mixed in unless `EmployeePOMixin`
// is also mixed in, otherwise compilation error.
@PageObject()
mixin TeacherPOMixin on EmployeePOMixin { ... }
@PageObject()
abstract class TenuredTeacherPO
with EmployeePOMixin, TeacherPOMixin {
// constructors and methods
}