generic uxd legos - selenium conference 2015

89
S Generic UXD Legos How to Build the Page Object API of Your Dreams By Selena Phillips | Akamai Technologies

Upload: selena-phillips

Post on 14-Feb-2017

1.155 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Generic UXD Legos - Selenium Conference 2015

S

Generic UXD LegosHow to Build the Page Object API of Your Dreams

By Selena Phillips | Akamai Technologies

Page 2: Generic UXD Legos - Selenium Conference 2015

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.

Page 3: Generic UXD Legos - Selenium Conference 2015

Table of Contents

Page Object API Requirements

Recipe for a New Component Model Specification Design Implementation

Examples Loadable Expandable Decorator Menu

Page 4: Generic UXD Legos - Selenium Conference 2015

S

Page Object API Requirements

Page 5: Generic UXD Legos - Selenium Conference 2015

API Requirements

Generic UXD Legos for composing page objectsA library of ready-to-use models of common UI components would significantly reduce the amount of boilerplate code.

Dynamically configurable page objectsIt 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.

Page 6: Generic UXD Legos - Selenium Conference 2015

API Requirements – page 2

Standardized approach for expanding the component libraryThere should be a virtually cookie-cutter approach for adding new components to the library.

Easy substitution of custom component implementationsBecause 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.

Page 7: Generic UXD Legos - Selenium Conference 2015

S

Recipe for a New Component Model

SpecificationDesign

Implementation

Page 8: Generic UXD Legos - Selenium Conference 2015

Recipe for a New Component Model: Specification

Identify the basic interface and interaction model for the component

Identify the minimal state that must be specified to construct the component

Identify the component's place in the inheritance hierarchy of other components

Identify the dynamically configurable behaviors to make the component robust across different environments

Page 9: Generic UXD Legos - Selenium Conference 2015

Recipe for a New Component Model: Design

Write a Java interface for the component

Write a Java interface for a state bean to specify the state necessary to instantiate the component

Write a Java interface for a fluent builder for the component

Write a Java interface for a configuration bean for the component

Page 10: Generic UXD Legos - Selenium Conference 2015

Recipe for a New Component Model: Implementation

Write a default implementation of the state bean

Write abstract and default implementations of the builder

Write an abstract and a default implementation of the component, if it makes sense

Write a default implementation of the configuration bean

Page 11: Generic UXD Legos - Selenium Conference 2015

S

Examples Loadable Expandable Decorator Menu

Page 12: Generic UXD Legos - Selenium Conference 2015

S

Examples: Loadable

Page 13: Generic UXD Legos - Selenium Conference 2015

Loadable: The most basic component

At the top of the inheritance hierarchy is the Loadable. This minimal component requires:

A WebDriver reference

A load timeout

Page 14: Generic UXD Legos - Selenium Conference 2015

LoadableBean: Loadable state bean

LoadableBean is a simple POJO for specifying the state necessary to construct a Loadable:

A WebDriver reference

A load timeout

Page 15: Generic UXD Legos - Selenium Conference 2015

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();}

Page 16: Generic UXD Legos - Selenium Conference 2015

LoadableConfig: Loadable configuration bean

LoadableConfig is a POJO for specifying environment sensitive state information for a Loadable.

Needs to configure the load timeout value which can be affected by the test environment

Needs to be deserializable with Jackson, so it can be instantiated with configuration data retrieved from an external datastore

Page 17: Generic UXD Legos - Selenium Conference 2015

LoadableConfig: Loadable configuration bean – page 2

Needs Optional fields so that a distinction can be made between a configuration value that is absent and one that is explicitly null

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.

Page 18: Generic UXD Legos - Selenium Conference 2015

public interface LoadableConfig { Class<? extends LoadableConfig> getType(); void setType(Class<? Extends LoadableConfig> type); Optional<Integer> getLoadTimeout(); void setLoadTimeout(Optional<Integer> timeout);}

Page 19: Generic UXD Legos - Selenium Conference 2015

LoadableBuilder: Fluent Loadable builder

Needs to be extensible because builders for more specialized components will extend it

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

Page 20: Generic UXD Legos - Selenium Conference 2015

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();}

Page 21: Generic UXD Legos - Selenium Conference 2015

LoadableBeanImpl:Default Loadable state bean

Uses Lombok to streamline the source code and reduce boilerplate code

Intializes fields with interface defaults where possible

Page 22: Generic UXD Legos - Selenium Conference 2015

public class LoadableBeanImpl implements LoadableBean {

private @Getter @Setter WebDriver driver; private @Getter @Setter int loadTimeout = DEFAULT_LOAD_TIMEOUT;}

Page 23: Generic UXD Legos - Selenium Conference 2015

LoadableConfigImpl: Default Loadable configuration bean

Uses Lombok to streamline the source code and reduce boilerplate code

Uses JSON as the external datastore

Page 24: Generic UXD Legos - Selenium Conference 2015

@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;}

…continued…

Page 25: Generic UXD Legos - Selenium Conference 2015

AbstractLoadableBuilder:Parent of all API builders

Uses Lombok to streamline the source code and reduce boilerplate code

Needs to be extensible because builder implementations for more specialized components will extend it

Needs to implement all methods in the LoadableBuilder interface because its main purpose is to be extended by builder implementations for more specialized components

Page 26: Generic UXD Legos - Selenium Conference 2015

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…

Page 27: Generic UXD Legos - Selenium Conference 2015

…AbstractLoadableBuilder continued…

public BuilderT setComponentClass(Class<LoadableT> clazz) { this.componentClass = clazz; return (BuilderT)this; }

public BuilderT setDriver(WebDriver driver) { getState().setDriver(driver); return (BuilderT)this; }

public BuilderT setLoadTimeout(int timeout) { getState().setLoadTimeout(timeout); return (BuilderT)this; }

…continued…

Page 28: Generic UXD Legos - Selenium Conference 2015

…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); }}

Page 29: Generic UXD Legos - Selenium Conference 2015

LoadableBuilderImpl:Default Loadable builder

Uses the default implementation of LoadableBean

Extends AbstractLoadableBuilder

Doesn’t need a reflexive generic type parameter in the class declaration because it is not intended to be extended

Needs to define only a constructor because AbstractLoadableBuilder implements all the methods in LoadableBuilder

Page 30: Generic UXD Legos - Selenium Conference 2015

//The only generic type parameter that is necessary for //this default builder is for the component it buildspublic class LoadableBuilderImpl<LoadableT extends Loadable> extends AbstractLoadableBuilder< LoadableT, LoadableBean, LoadableBuilderImpl<LoadableT> > {

//Use the default LoadableBean implementation public LoadableBuilderImpl() { super(new LoadableBeanImpl()); }}

Page 31: Generic UXD Legos - Selenium Conference 2015

AbstractLoadable: Parent of all components

Uses Lombok to reduce boilerplate

Extends SlowLoadableComponent

Assumes child classes annotate their WebElements with locators, so uses PageFactory in load()

Provides the means to query a configuration service for a configuration by ID

Page 32: Generic UXD Legos - Selenium Conference 2015

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…

Page 33: Generic UXD Legos - Selenium Conference 2015

…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); } }}

Page 34: Generic UXD Legos - Selenium Conference 2015

ConfigService: Dynamic configuration service

Needs to provide a means to set the active configuration profile

Needs to provide a means to query the active configuration profile for a component configuration by ID

Page 35: Generic UXD Legos - Selenium Conference 2015

public interface ConfigService { void setProfile(String profile); String getProfile(); <ConfigT extends LoadableConfig> ConfigT getConfig(String id);}

Page 36: Generic UXD Legos - Selenium Conference 2015

ConfigServiceImpl: Default configuration service

Is a singleton class with a static accessor method for the singleton instance

Expects configuration profiles to be in the resources/pageobject_config folder

Expects a filename to be specifed for locating a configuration profile in the above folder

Expects a map data structure in the JSON source

Page 37: Generic UXD Legos - Selenium Conference 2015

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…

Page 38: Generic UXD Legos - Selenium Conference 2015

…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…

Page 39: Generic UXD Legos - Selenium Conference 2015

…ConfigServiceImpl continued…

public <ConfigT extends LoadableConfig> ConfigT getConfig(String id) {

return (ConfigT)configs.get(id); }}

Page 40: Generic UXD Legos - Selenium Conference 2015

//Example configuration JSON source. Three //components have configurations for their load//timeout values{ “com.akamai.SomeLoadable":{ "type":"com.akamai.LoadableConfigImpl", ”loadTimeout”:120 }, “com.akamai.SomeOtherLoadable”:{ "type":"com.akamai.LoadableConfigImpl", ”loadTimeout”:90 }, “com.akamai.YetAnotherLoadable”:{ "type":"com.akamai.LoadableConfigImpl", ”loadTimeout”:45 }}

Page 41: Generic UXD Legos - Selenium Conference 2015

S

Examples: Expandable

Page 42: Generic UXD Legos - Selenium Conference 2015

Expandable component: Specification

An expandable component’s visibility can be toggled

Components that can be dismissed cannot necessarily be accessed. Example: Announcement dialogs

Components that can be accessed or dismissed need some mechanism for polling their visibility

An expandable component has a content pane that is visible when it is expanded and not visible when it is collapsed

Page 43: Generic UXD Legos - Selenium Conference 2015

Expandable component: Design

A polleable component needs to be separated out as a component type which can be extended by more specialized component types

A content pane needs to be separated out as a component type which can be extended by more specialized components

Accessible and dismissable components need to be modeled separately

Page 44: Generic UXD Legos - Selenium Conference 2015

Expandable component: Design – page 2

It is possible for controls to access and dismiss components to be hoverable

Selenium native hover and click actions are prone to silent failure in some environments, so the use of workarounds needs to be dynamically configurable

Use marker interfaces that extend interfaces for accessible and dismissable components to specify components that are both

Page 45: Generic UXD Legos - Selenium Conference 2015

//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();}

Page 46: Generic UXD Legos - Selenium Conference 2015

//The time for a component to reach an expected state is //environment sensitivepublic 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);}

Page 47: Generic UXD Legos - Selenium Conference 2015

public interface ElementWrapperBean extends LoadableBean { void setWrappedElement(WebElement e); WebElement getWrappedElement();}

public interface ElementWrapperBuilder< LoadableT extends Loadable, BeanT extends ElementWrapperBean, BuilderT extends ElementWrapperBuilder< LoadableT, BeanT, BuilderT> > extends LoadableBuilder<LoadableT, BeanT, BuilderT> {

BuilderT setWrappedElement(WebElement wrappedElement);}

Page 48: Generic UXD Legos - Selenium Conference 2015

public interface AccessibleBean extends ElementWrapperBean, PolleableBean {

void setAccessor(WebElement accessor); WebElement getAccessor(); void setAccessorHoverable(boolean hoverable); boolean isAccessorHoverable(); void setHoverAccessorWithJavascript(boolean hoverWithJavascript); boolean getHoverAccessorWithJavascript(); void setClickAccessorWithJavascript(boolean clickWithJavascript); boolean getClickAccessorWithJavascript();}

Page 49: Generic UXD Legos - Selenium Conference 2015

//Selenium’s native click and hover actions are //environment sensitive, so the the use of //workarounds should be dynamically configurablepublic interface AccessibleConfig extends PolleableConfig { Optional<Boolean> getHoverAccessorWithJavascript(); void setHoverAccessorWithJavascript(Optional<Boolean> hoverWithJavascript); Optional<Boolean> getClickAccessorWithJavascript(); void setClickAccessorWithJavascript(Optional<Boolean> clickWithJavascript);}

Page 50: Generic UXD Legos - Selenium Conference 2015

public interface AccessibleBuilder< PolleableT extends Polleable, BeanT extends AccessibleBean, BuilderT extends AccessibleBuilder< PolleableT,BeanT,BuilderT>> extends PolleableBuilder<PolleableT,BeanT,BuilderT>, ElementWrapperBuilder<PolleableT,BeanT,BuilderT> {

BuilderT setAccessor(WebElement accessor); BuilderT setAccessorHoverable(boolean hoverable); BuilderT setHoverAccessorWithJavascript(boolean hoverWithJavascript); BuilderT setClickAccessorWithJavascript(boolean clickWithJavascript);}

Page 51: Generic UXD Legos - Selenium Conference 2015

//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 visiblepublic 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();}

Page 52: Generic UXD Legos - Selenium Conference 2015

//The configuration bean for a component that can be//dismissed is a mirror to the one for a component that //can be rendered visiblepublic interface DismissableConfig extends PolleableConfig { Optional<Boolean> getHoverDismisserWithJavascript(); void setHoverDismisserWithJavascript(Optional<Boolean> hoverWithJavascript); Optional<Boolean> getClickDismisserWithJavascript(); void setClickDismisserWithJavascript(Optional<Boolean> clickWithJavascript);}

Page 53: Generic UXD Legos - Selenium Conference 2015

public interface DismissableBuilder< PolleableT extends Polleable, BeanT extends DismissableBean, BuilderT extends DismissableBuilder< PolleableT, BeanT, BuilderT>> extends PolleableBuilder<PolleableT, BeanT, BuilderT>, ElementWrapperBuilder<PolleableT,BeanT,BuilderT> {

BuilderT setDismisser(WebElement dismisser); BuilderT setDismisserHoverable(boolean hoverable); BuilderT setHoverDismisserWithJavascript(boolean hoverWithJavascript); BuilderT setClickDismisserWithJavascript(boolean clickWithJavaScript);}

Page 54: Generic UXD Legos - Selenium Conference 2015

public interface ToggleableVisibilityBean extends DismissableBean, AccessibleBean { }

public interface ToggleableVisibilityConfig extends AccessibleConfig, DismissableConfig { }

public interface ToggleableVisibilityBuilder< PolleableT extends Polleable, BeanT extends ToggleableVisibilityBean, BuilderT extends ToggleableVisibilityBuilder <PolleableT,BeanT,BuilderT>> extends AccessibleBuilder<PolleableT,BeanT,BuilderT>, DismissableBuilder<PolleableT,BeanT,BuilderT> { }

Page 55: Generic UXD Legos - Selenium Conference 2015

public interface Expandable extends Polleable { void expand(); void collapse(); boolean isExpanded(); boolean isCollapsed();}

Page 56: Generic UXD Legos - Selenium Conference 2015

Expandable component: Implementation

Depends on the abstract and concrete implementations for a Polleable

Depends on the abstract implementation for an ElementWrapper

Expandable should be modeled in both abstract and concrete implementations because expandable behavior is usable by even more specialized component types

Page 57: Generic UXD Legos - Selenium Conference 2015

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(); }}

Page 58: Generic UXD Legos - Selenium Conference 2015

//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; }}

Page 59: Generic UXD Legos - Selenium Conference 2015

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)); } }}

Page 60: Generic UXD Legos - Selenium Conference 2015

//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…

Page 61: Generic UXD Legos - Selenium Conference 2015

…AbstractExpandable continued…

public boolean isExpanded() { try { return getContainer().isDisplayed(); } catch(NoSuchElementException e) { return false; } } public boolean isCollapsed() { return !isExpanded(); }

…continued…

Page 62: Generic UXD Legos - Selenium Conference 2015

…AbstractExpandable continued…

public void expand() { if(!isExpanded() && isAccessorHoverable()) {

hoverControl(getAccessor(), hoverAccessorWithJavascript()); clickControl(getAccessor(), clickAccessorWithJavascript(), true); ComponentLoader.waitForExpand(this); }

if(!isExpanded()) { clickControl(getAccessor(), clickAccessorWithJavascript(), true); ComponentLoader.waitForExpand(this); } }

continued…

Page 63: Generic UXD Legos - Selenium Conference 2015

…AbstractExpandable continued…

public void collapse() { if(!isCollapsed() && isDismisserHoverable()) {

hoverControl(getDismisser(), hoverDismisserWithJavascript()); clickControl(getDismisser(), clickDismisserWithJavascript(), true); ComponentLoader.waitForCollapse(this); }

if(!isCollapsed()) { clickControl(getDismisser(), clickDismisserWithJavascript(), true); ComponentLoader.waitForCollapse(this); } }

continued…

Page 64: Generic UXD Legos - Selenium Conference 2015

…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…

Page 65: Generic UXD Legos - Selenium Conference 2015

…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…

Page 66: Generic UXD Legos - Selenium Conference 2015

…AbstractExpandable continued… protected abstract WebElement getAccessor(); protected abstract WebElement getDismisser(); protected abstract boolean isAccessorHoverable(); protected abstract boolean isDismisserHoverable(); protected abstract boolean hoverAccessorWithJavascript(); protected abstract boolean hoverDismisserWithJavascript(); protected abstract boolean clickAccessorWithJavascript(); protected abstract boolean clickDismisserWithJavascript();

…continued…

Page 67: Generic UXD Legos - Selenium Conference 2015

…AbstractExpandable continued…

protected void isLoaded() throws Error { try { if(!isAccessorHoverable()) { assertTrue(getAccessor().isDisplayed()); } else { //If this throws a NoSuchElementException, //the Expandable is not loaded getAccessor().isDisplayed(); } } catch(NoSuchElementException e) { throw new Error(e)); } }}

Page 68: Generic UXD Legos - Selenium Conference 2015

public class ExpandableImpl extends AbstractExpandable<ExpandableImpl> {

private @Getter(value = AccessLevel.PROTECTED) WebElement accessor; private @Getter(value = AccessLevel.PROTECTED) WebElement dismisser; private @Getter(value = AccessLevel.PROTECTED) WebElement container; private @Getter(value = AccessLevel.PROTECTED) boolean isAccessorHoverable; private @Getter(value = AccessLevel.PROTECTED) boolean isDismisserHoverable; //Cannot use Lombok because getters for these booleans //do not have ‘is’ as prefix, but ‘get’ private boolean hoverAccessorWithJavascript; private boolean hoverDismisserWithJavascript; private boolean clickAccessorWithJavascript; private boolean clickDismisserWithJavascript;

…continued…

Page 69: Generic UXD Legos - Selenium Conference 2015

…ExpandableImpl continued…

public ExpandableImpl(ToggleableVisibilityBean bean) { super(bean); this.accessor = bean.getAccessor(); this.dismisser = bean.getDismisser (); this.containerElement = bean.getWrappedElement(); this.isAccessorHoverable = bean.isAccessorHoverable(); this.isDismisserHoverable = bean.isDismisserHoverable(); this.hoverAccessorWithJavascript = bean.getHoverAccessorWithJavascript(); this.hoverDismisserWithJavascript = bean.getHoverDismisserWithJavascript(); this.clickAccessorWithJavascript = bean.getClickAccessorWithJavascript(); this.clickDismisserWithJavascript = bean.getClickDismisserWithJavascript(); } //Omitting the getters for the booleans }

…continued…

Page 70: Generic UXD Legos - Selenium Conference 2015

S

Examples: Decorator

Page 71: Generic UXD Legos - Selenium Conference 2015

Decorator:New features without boilerplate

A component implementation can inherit from only one class, but may need to combine behavior from multiple component types

Pre-Java 8 interfaces allow this type of combination, but have the drawback of requiring lots of duplicated code

Java 8 allows interfaces with default method implementations, so you can define the behavior in a Decorator interface

A class can implement multiple Decorator interfaces with default method implementations, thereby adding functionality without boilerplate code

Page 72: Generic UXD Legos - Selenium Conference 2015

public interface PolleableDecorator extends Polleable {

LoadableProvider<? extends Polleable> getPolleableProvider();

default int getPollingTimeout() { return getPolleableProvider() .getLoadable() .getPollingTimeout(); }

default int getPollingInterval() { return getPolleableProvider() .getLoadable() .getPollingInterval(); }}

Page 73: Generic UXD Legos - Selenium Conference 2015

public class LoadableProvider<LoadableT extends Loadable> {

private @Getter(value = AccessLevel.PROTECTED) LoadableT loadable;

public LoadableProvider(LoadableT loadable) { this.loadable = loadable; }}

Page 74: Generic UXD Legos - Selenium Conference 2015

public interface ExpandableDecorator extends Expandable {

LoadableProvider<? extends Expandable> getExpandableProvider();

default void expand() { getExpandableProvider() .getLoadable() .expand(); }

default void collapse() { getExpandableProvider() .getLoadable() .collapse(); }

…continued…

Page 75: Generic UXD Legos - Selenium Conference 2015

…ExpandableDecorator continued…

default boolean isExpanded() { return getExpandableProvider() .getLoadable() .isExpanded(); }

default boolean isCollapsed() { return getExpandableProvider() .getLoadable() .isCollapsed(); }

default int getPollingTimeout() { return getExpandableProvider() .getLoadable() .getPollingTimeout(); }

…continued…

Page 76: Generic UXD Legos - Selenium Conference 2015

…ExpandableDecorator continued…

default int getPollingInterval() { return getExpandableProvider() .getLoadable() .getPollingInterval(); }}

Page 77: Generic UXD Legos - Selenium Conference 2015

S

Examples: Menu

Page 78: Generic UXD Legos - Selenium Conference 2015

Menu component: Specification

Menus have a content container that contains their options

Menus can be flat or expandable

A menu component needs to provide a getter for the available options

A menu component needs to provide the ability to click an option

Page 79: Generic UXD Legos - Selenium Conference 2015

Menu component: Design

The basic menu interface should be agnostic to whether it is implemented by a flat or dropdown menu component

Decorators and marker interfaces should be used to define a dropdown menu

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

Page 80: Generic UXD Legos - Selenium Conference 2015

public interface Menu<OptionT extends Clickable> extends Loadable { List<OptionT> getOptions(); void clickOption(final OptionT option);}

public interface MenuBean extends ElementWrapperBean { void setOptionElements(List<WebElement> options); List<WebElement> getOptionElements(); void setClickOptionWithJavascript(boolean clickWithJavascript); boolean getClickOptionWithJavascript();}

public interface MenuConfig extends LoadableConfig { Optional<Boolean> getClickOptionWithJavascript(); void setClickOptionWithJavascript(Optional<Boolean> clickWithJavascript);}

Page 81: Generic UXD Legos - Selenium Conference 2015

public interface MenuBuilder< MenuT extends Menu<? extends Clickable>, BeanT extends MenuBean, BuilderT extends MenuBuilder<MenuT,BeanT,BuilderT> > extends ElementWrapperBuilder<MenuT,BeanT,BuilderT> { BuilderT setOptionElements(List<WebElement> options); BuilderT setClickOptionWithJavascript(boolean clickWithJavascript);}

Page 82: Generic UXD Legos - Selenium Conference 2015

public interface DropDownMenuBean extends MenuBean, ToggleableVisibilityBean { }

public interface DropDownMenuConfig extends MenuConfig, ToggleableVisibilityConfig { }

public interface DropDownMenuBuilder< MenuT extends Menu<? extends Clickable> & Expandable, BeanT extends DropDownMenuBean, BuilderT extends DropDownMenuBuilder< MenuT,BeanT,BuilderT> > extends MenuBuilder<MenuT,BeanT,BuilderT>, ToggleableVisibilityBuilder<MenuT, BeanT,BuilderT> { }

Page 83: Generic UXD Legos - Selenium Conference 2015

public abstract class AbstractMenu< OptionT extends Clickable, MenuT extends AbstractMenu<OptionT,MenuT>> extends AbstractContentPane<MenuT> implements Menu<OptionT> {

public AbstractMenu(LoadableBean bean) { super(bean); }

public List<OptionT> getOptions() { if (isDropDownMenu()) { ((Expandable) this).expand(); }

List<OptionT> options = buildOptions();

if (isDropDownMenu()) { ((Expandable) this).collapse(); }

return options; } continued…

Page 84: Generic UXD Legos - Selenium Conference 2015

…AbstractMenu continued…

public void clickOption(OptionT option) { if(getOptions().contains(option)) { if(isDropDownMenu()) { ((Expandable)this).expand(); } option.click();

if(isDropDownMenu()) { ((Expandable) this).collapse(); } } else { throw new IllegalArgumentException(); } }

…continued…

Page 85: Generic UXD Legos - Selenium Conference 2015

…AbstractMenu continued…

protected abstract List<WebElement> getOptionElements(); protected abstract boolean clickOptionWithJavascript();

protected List<OptionT> buildOptions() { final List<OptionT> options = new ArrayList<>();

for(WebElement element : getOptionElements()) { options.add(buildOption(element)); }

return options; }

protected boolean isDropDownMenu() { return Expandable.class .isAssignableFrom(this.getClass()); }continued…

Page 86: Generic UXD Legos - Selenium Conference 2015

…AbstractMenu continued…

protected ClickableBuilder<OptionT,? extends ClickableBean,?> getOptionBuilder() { return new ClickableBuilderImpl<>(); }

private OptionT buildOption(WebElement element) { return getOptionBuilder() .setComponentClass(getOptionClass()) .setDriver(getDriver()) .setWrappedElement(optionElement) .setUseJavascriptClick( clickOptionWithJavascript()) .build()); }

private Class<OptionT> getOptionClass() { //Omitted for brevity. Fancy Guava //reflection API is used here. }}

Page 87: Generic UXD Legos - Selenium Conference 2015

public class DropDownBasicMenu extends AbstractBasicMenu<DropDownBasicMenu> implements ExpandableDecorator {

private @Getter LoadableProvider<? Extends Expandable> expandableProvider; private @Getter(value = AccessLevel.PROTECTED) List<WebElement> optionElements; private @Getter(value = AccessLevel.PROTECTED) WebElement container; private @Getter(value = AccessLevel.PRIVATE) WebElement accessor; private boolean clickOptionWithJavascript;

…continued…

Page 88: Generic UXD Legos - Selenium Conference 2015

…DropDownBasicMenu continued…

public DropDownBasicMenu(DropDownMenuBean bean) { super(bean); this.expandableProvider = new LoadableProvider<>(new ExpandableImpl(bean)); this.optionElements = bean.getOptionElements(); this.container = bean.getWrappedElement(); this.accessor = bean.getAccessor(); this.clickOptionWithJavascript = bean.getClickOptionWithJavascript(); }

protected boolean clickOptionWithJavascript() { return clickOptionWithJavascript; }

…continued…

Page 89: Generic UXD Legos - Selenium Conference 2015

…DropDownBasicMenu continued…

protected void isLoaded() throws Error { try { expandableProvider.getLoadable().isLoaded(); } catch(NoSuchElementException e) { throw new Error(e)); } }}