1. S
Generic UXD Legos
How to Build the Page Object API of Your Dreams
By Selena Phillips | Akamai Technologies
2. Introduction
The Page Object design pattern has always had some
weaknesses. Refinements on basic design pattern have emerged
to address some of these shortcomings – the
SlowLoadableComponent pattern and the practice of modeling
the business layer separately from the UI layer, for example.
These refinements don’t make developing page objects any easier
or faster, nor do they provide a solution to the frustrating inevitability
that some page object interactions work perfectly in some
environments while failing silently in others.
3. Table of
Contents
S Page Object API
Requirements
S Recipe for a New
Component Model
S Specification
S Design
S Implementation
S Examples
S Loadable
S Expandable
S Decorator
S Menu
5. API Requirements
S Generic UXD Legos for composing page objects
A library of ready-to-use models of common UI components
would significantly reduce the amount of boilerplate code.
S Dynamically configurable page objects
It should be possible to specify that your page object should
click that pesky button using a JavaScript workaround for the
environments where it fails silently without boilerplate if-then-
else clauses.
6. API Requirements – page 2
S Standardized approach for expanding the component
library
There should be a virtually cookie-cutter approach for adding
new components to the library.
S Easy substitution of custom component implementations
Because there is no such thing as one-size-fits-all, it should be
trivial to substitute a custom implementation of a component
type that utilizes the same basic interface as the default.
7. S
Recipe for a New
Component Model
Specification
Design
Implementation
8. Recipe for a New Component Model:
Specification
S Identify the basic interface and interaction model for the
component
S Identify the minimal state that must be specified to construct
the component
S Identify the component's place in the inheritance hierarchy of
other components
S Identify the dynamically configurable behaviors to make the
component robust across different environments
9. Recipe for a New Component Model:
Design
S Write a Java interface for the component
S Write a Java interface for a state bean to specify the state
necessary to instantiate the component
S Write a Java interface for a fluent builder for the component
S Write a Java interface for a configuration bean for the
component
10. Recipe for a New Component Model:
Implementation
S Write a default implementation of the state bean
S Write abstract and default implementations of the builder
S Write an abstract and a default implementation of the
component, if it makes sense
S Write a default implementation of the configuration bean
13. Loadable:
The most basic component
At the top of the inheritance hierarchy is the Loadable. This
minimal component requires:
S A WebDriver reference
S A load timeout
15. public interface Loadable {
int DEFAULT_LOAD_TIMEOUT = 30;
WebDriver getDriver();
int getLoadTimeout();
}
public interface LoadableBean {
void setDriver(WebDriver driver);
WebDriver getDriver();
void setLoadTimeout(int timeout);
int getLoadTimeout();
}
16. LoadableConfig:
Loadable configuration bean
LoadableConfig is a POJO for specifying environment sensitive
state information for a Loadable.
S Needs to configure the load timeout value which can be
affected by the test environment
S Needs to be deserializable with Jackson, so it can be
instantiated with configuration data retrieved from an external
datastore
17. LoadableConfig:
Loadable configuration bean – page 2
S Needs Optional fields so that a distinction can be made
between a configuration value that is absent and one that is
explicitly null
S Needs to specify type information because configuration beans
for more specialized components extend beans for less
specialized components. Jackson needs this type information
to properly handle the polymorphism.
19. LoadableBuilder:
Fluent Loadable builder
S Needs to be extensible because builders for more specialized
components will extend it
S Needs to have setters that return the runtime type of the
builder, so that they can be called in any order because a
complex component requires specification of numerous state
fields before it can be instantiated
20. public interface LoadableBuilder<
LoadableT extends Loadable,
BeanT extends LoadableBean,
//The reflexive parameter makes it possible to
//return the runtime type of the builder
BuilderT extends
LoadableBuilder<LoadableT,BeanT, BuilderT>> {
BeanT getState();
BuilderT setComponentClass(Class<LoadableT> clazz);
Class<LoadableT> getComponentClass();
BuilderT setDriver(WebDriver driver);
BuilderT setLoadTimeout(int timeout);
LoadableT build();
}
21. LoadableBeanImpl:
Default Loadable state bean
S Uses Lombok to streamline the source code and reduce
boilerplate code
S Intializes fields with interface defaults where possible
22. public class LoadableBeanImpl implements
LoadableBean {
private @Getter @Setter WebDriver driver;
private @Getter @Setter int loadTimeout =
DEFAULT_LOAD_TIMEOUT;
}
24. @JsonTypeInfo(
//Determines how type information is specified in the
//source JSON
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "type”, visible = true)
@JsonSubTypes({
@Type(value = PolleableConfigImpl.class),
//Omitted for brevity. All sub-types of
//LoadableConfigImpl must be listed })
public class LoadableConfigImpl implements
LoadableConfig {
//The type field must be present in the source JSON.
@JsonProperty(required = true)
private @Getter @Setter Class<? extends
LoadableConfig> type;
@JsonProperty
private @Getter @Setter Optional<Integer> loadTimeout;
}
25. AbstractLoadableBuilder:
Parent of all API builders
S Uses Lombok to streamline the source code and reduce
boilerplate code
S Needs to be extensible because builder implementations for
more specialized components will extend it
S Needs to implement all methods in the LoadableBuilder
interface because its main purpose is to be extended by builder
implementations for more specialized components
26. public abstract class AbstractLoadableBuilder<
LoadableT extends Loadable,
BeanT extends LoadableBean,
//Use the same reflexive parameter that is used in
//the LoadableBuilder interface because this class
//is intended to be extended by more specialized
//builder implementations, just as the interface
//is intended to be extended by interfaces for
//more specialized builders
BuilderT extends
LoadableBuilder<LoadableT,BeanT,BuilderT>>
implements LoadableBuilder<LoadableT,BeanT,BuilderT> {
private @Getter BeanT state;
private @Getter Class<LoadableT> componentClass;
public AbstractLoadableBuilder(BeanT bean) {
this.state = bean;
}
…continued…
28. …AbstractLoadableBuilder continued…
//The factory and its interface are not shown in this
//presentation. The default implementation is a
//singleton class with a static accessor method for
//the singleton instance. To substitute a non-default
//implementation of the interface, a sub-class can
//override this method.
protected LoadableFactory getFactory() {
return LoadableFactoryImpl.getInstance();
}
//The default factory implementation uses reflection
//to get a constructor for the component. Therefore,
//the component must define a single-arg constructor
//that accepts the state bean as a parameter.
public LoadableT build() {
return getFactory().create(getState(),
componentClass);
}
}
29. LoadableBuilderImpl:
Default Loadable builder
S Uses the default implementation of LoadableBean
S Extends AbstractLoadableBuilder
S Doesn’t need a reflexive generic type parameter in the class
declaration because it is not intended to be extended
S Needs to define only a constructor because
AbstractLoadableBuilder implements all the methods in
LoadableBuilder
30. //The only generic type parameter that is necessary for
//this default builder is for the component it builds
public class LoadableBuilderImpl<LoadableT extends
Loadable>
extends AbstractLoadableBuilder<
LoadableT,
LoadableBean,
LoadableBuilderImpl<LoadableT>
> {
//Use the default LoadableBean implementation
public LoadableBuilderImpl() {
super(new LoadableBeanImpl());
}
}
31. AbstractLoadable:
Parent of all components
S Uses Lombok to reduce boilerplate
S Extends SlowLoadableComponent
S Assumes child classes annotate their WebElements with
locators, so uses PageFactory in load()
S Provides the means to query a configuration service for a
configuration by ID
32. public abstract class AbstractLoadable<LoadableT
extends AbstractLoadable<LoadableT>>
extends SlowLoadableComponent<LoadableT>
implements Loadable {
private @Getter WebDriver driver;
private @Getter int loadTimeout;
//Use a state bean to provide all the state
//data necessary to instantiate a component
public AbstractLoadable(LoadableBean bean) {
super(new SystemClock(),
bean.getLoadTimeout());
this.driver = bean.getDriver();
this.loadTimeout =bean.getLoadTimeout();
}
protected void load() {
PageFactory.initElements(getDriver(), this);
}
…continued…
33. …AbstractLoadableComponent continued…
//A non-default implementation can be substituted by
//overriding this method.
protected ConfigService getConfigService() {
return ConfigServiceImpl.getInstance();
}
protected <ConfigT extends LoadableConfig> ConfigT
getConfig(String id) {
//A profile is a collection of configurations
//based on environment.
if(getConfigService().getProfile() != null) {
return service.getConfig(id);
}
}
}
34. ConfigService:
Dynamic configuration service
S Needs to provide a means to set the active configuration profile
S Needs to provide a means to query the active configuration
profile for a component configuration by ID
36. ConfigServiceImpl:
Default configuration service
S Is a singleton class with a static accessor method for the
singleton instance
S Expects configuration profiles to be in the
resources/pageobject_config folder
S Expects a filename to be specifed for locating a configuration
profile in the above folder
S Expects a map data structure in the JSON source
37. public class ConfigServiceImpl implements ConfigService {
//Filename of the current configuration profile, based
//on the OS, browser and browser version. The file is
//a JSON file with a map structure, with a String id
//for each component configuration in the file
private @Getter String profile;
private Map<String,LoadableConfig> configs =
new HashMap<>();
private static final class Loader {
private static final ConfigServiceImpl INSTANCE =
new ConfigServiceImpl();
}
private ConfigServiceImpl() { }
public static ConfigServiceImpl getInstance() {
return Loader.INSTANCE;
}
…continued…
38. …ConfigServiceImpl continued…
public void setProfile(String profile) {
this.profile = profile;
URL configUrl = ConfigServiceImpl.class
.getClassLoader()
.getResource("./pageobject_config/” + profile);
ObjectMapper mapper = new ObjectMapper()
.registerModule(new Jdk8Module());
//Give type info to Jackson for the Map field
MapType mapType = mapper.getTypeFactory()
.constructMapType(HashMap.class, String.class,
LoadableConfigImpl.class);
//try-catch blocks omitted for brevity
File configFile = new File(configUrl.toURI());
configs = mapper.readValue(configFile, mapType);
}
…continued…
42. Expandable component:
Specification
S An expandable component’s visibility can be toggled
S Components that can be dismissed cannot necessarily be
accessed. Example: Announcement dialogs
S Components that can be accessed or dismissed need some
mechanism for polling their visibility
S An expandable component has a content pane that is visible
when it is expanded and not visible when it is collapsed
43. Expandable component:
Design
S A polleable component needs to be separated out as a
component type which can be extended by more specialized
component types
S A content pane needs to be separated out as a component type
which can be extended by more specialized components
S Accessible and dismissable components need to be modeled
separately
44. Expandable component:
Design – page 2
S It is possible for controls to access and dismiss components to
be hoverable
S Selenium native hover and click actions are prone to silent
failure in some environments, so the use of workarounds needs
to be dynamically configurable
S Use marker interfaces that extend interfaces for accessible and
dismissable components to specify components that are both
45. //A component that is polled for a state change needs a
//polling timeout and a polling interval
public interface Polleable extends Loadable {
int DEFAULT_POLLING_TIMEOUT = 30;
int DEFAULT_POLLING_INTERVAL = 1;
int getPollingTimeout();
int getPollingInterval();
}
public interface PolleableBean extends LoadableBean {
void setPollingTimeout(int timeout);
int getPollingTimeout();
void setPollingInterval(int timeout);
int getPollingInterval();
}
46. //The time for a component to reach an expected state is
//environment sensitive
public interface PolleableConfig extends LoadableConfig {
Optional<Integer> getPollingTimeout();
void setPollingTimeout(Optional<Integer> timeout);
Optional<Integer> getPollingInterval();
void setPollingInterval(Optional<Integer> interval);
}
public interface PolleableBuilder<
PolleableT extends Polleable,
BeanT extends PolleableBean,
BuilderT extends PolleableBuilder<PolleableT,
BeanT, BuilderT>
> extends LoadableBuilder<PolleableT, BeanT,
BuilderT> {
BuilderT setPollingTimeout(int timeout);
BuilderT setPollingInterval(int timeout);
}
49. //Selenium’s native click and hover actions are
//environment sensitive, so the the use of
//workarounds should be dynamically configurable
public interface AccessibleConfig extends PolleableConfig {
Optional<Boolean> getHoverAccessorWithJavascript();
void setHoverAccessorWithJavascript(Optional<Boolean>
hoverWithJavascript);
Optional<Boolean> getClickAccessorWithJavascript();
void setClickAccessorWithJavascript(Optional<Boolean>
clickWithJavascript);
}
51. //The state bean for a component that can be dismissed
//from view is a mirror image to the state bean for a
//component that can be rendered visible
public interface DismissableBean extends PolleableBean,
ElementWrapperBean {
void setDismisser(WebElement dismisser);
WebElement getDismisser();
void setDismisserHoverable(boolean hoverable);
boolean isDismisserHoverable();
void setHoverDismisserWithJavascript(boolean
hoverWithJavascript);
boolean getHoverDismisserWithJavascript();
void setClickDismisserWithJavascript(boolean
clickWithJavascript);
boolean getClickDismisserWithJavascript();
}
52. //The configuration bean for a component that can be
//dismissed is a mirror to the one for a component that
//can be rendered visible
public interface DismissableConfig extends PolleableConfig {
Optional<Boolean> getHoverDismisserWithJavascript();
void setHoverDismisserWithJavascript(Optional<Boolean>
hoverWithJavascript);
Optional<Boolean> getClickDismisserWithJavascript();
void setClickDismisserWithJavascript(Optional<Boolean>
clickWithJavascript);
}
56. Expandable component:
Implementation
S Depends on the abstract and concrete implementations for a
Polleable
S Depends on the abstract implementation for an
ElementWrapper
S Expandable should be modeled in both abstract and concrete
implementations because expandable behavior is usable by
even more specialized component types
57. public abstract class AbstractPolleable<PolleableT extends
AbstractPolleable<PolleableT>>
extends AbstractLoadable<PolleableT>
implements Polleable {
private @Getter int pollingTimeout;
private @Getter int pollingInterval;
public AbstractPolleable(PolleableBean bean) {
super(bean);
this.pollingTimeout = bean.getPollingTimeout();
this.pollingInterval = bean.getPollingInterval();
}
}
58. //The concrete implementation only requires a constructor
//because all the methods of the Polleable interface are
//implemented in the abstract parent class.
public class PolleableImpl extends
AbstractPolleable<PolleableImpl> {
public PolleableImpl(PolleableBean bean) {
super(bean);
}
protected void isLoaded() throws Error {
//Do nothing;
}
}
59. public abstract class AbstractContentPane<ContentPaneT
extends AbstractContentPane<ContentPaneT>>
extends AbstractLoadable<ContentPaneT> {
public AbstractContentPane(LoadableBean bean) {
super(bean);
}
protected abstract WebElement getContainer();
protected void isLoaded() throws Error {
try {
assertTrue(getContainer().isDisplayed());
} catch(NoSuchElementException e) {
throw new Error(e));
}
}
}
60. //An Expandable is both a content pane and a polleable
//component, but it can inherit from only one of
//AbstractContentPane and AbstractPolleable. Decorators
//allow for components like Expandable to implement
//behavior from multiple component types without a lot of
//extra code. More on this to come.
public abstract class AbstractExpandable<ExpandableT
extends AbstractExpandable<ExpandableT>>
extends AbstractContentPane<ExpandableT>
implements Expandable, PolleableDecorator {
private @Getter LoadableProvider<? Extends Polleable>
polleableProvider;
public AbstractExpandable(PolleableBean bean) {
super(bean);
this.polleableProvider =
new LoadableProvider<>(new PolleableImpl(bean));
}
…continued…
64. …AbstractExpandable continued…
private void hoverOverControl(WebElement control,
boolean useJavascript) {
if(useJavascript) {
//Lambda function for using JavascriptExecutor
//to hover over the control
new HoverWithJavascript().accept(getDriver(),
control);
} else {
Actions action = new Actions(getDriver());
action.moveToElement(control).perform();
}
}
…continued…
65. …AbstractExpandable continued…
private void clickControl(WebElement control, boolean
useJavascript, boolean isControlHoverable) {
if(useJavascript) {
//Lambda function to use JavascriptExecutor to
//click control
new ClickWithJavascript().accept(getDriver(),
control);
} else if(isControlHoverable) {
Actions action = new Actions(getDriver());
action.click(control).perform();
} else { control.click(); }
}
…continued…
71. Decorator:
New features without boilerplate
S A component implementation can inherit from only one class,
but may need to combine behavior from multiple component
types
S Pre-Java 8 interfaces allow this type of combination, but have
the drawback of requiring lots of duplicated code
S Java 8 allows interfaces with default method implementations,
so you can define the behavior in a Decorator interface
S A class can implement multiple Decorator interfaces with
default method implementations, thereby adding functionality
without boilerplate code
72. public interface PolleableDecorator extends
Polleable {
LoadableProvider<? extends Polleable>
getPolleableProvider();
default int getPollingTimeout() {
return getPolleableProvider()
.getLoadable()
.getPollingTimeout();
}
default int getPollingInterval() {
return getPolleableProvider()
.getLoadable()
.getPollingInterval();
}
}
73. public class LoadableProvider<LoadableT extends
Loadable> {
private @Getter(value = AccessLevel.PROTECTED) LoadableT
loadable;
public LoadableProvider(LoadableT loadable) {
this.loadable = loadable;
}
}
78. Menu component:
Specification
S Menus have a content container that contains their options
S Menus can be flat or expandable
S A menu component needs to provide a getter for the available
options
S A menu component needs to provide the ability to click an
option
79. Menu component:
Design
S The basic menu interface should be agnostic to whether it is
implemented by a flat or dropdown menu component
S Decorators and marker interfaces should be used to define a
dropdown menu
S The click action for a menu option can fail silently in some
environments, so the use of a JavaScript workaround to click
an option should be dynamically configurable