Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Generic UXD Legos - Selenium Conference 2015

1,219 views

Published on

A source-code heavy guide to building a page object API for Selenium automation test suite,

Published in: Software
  • Be the first to comment

Generic UXD Legos - Selenium Conference 2015

  1. 1. S Generic UXD Legos How to Build the Page Object API of Your Dreams By Selena Phillips | Akamai Technologies
  2. 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. 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
  4. 4. S Page Object API Requirements
  5. 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. 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. 7. S Recipe for a New Component Model Specification Design Implementation
  8. 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. 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. 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
  11. 11. S Examples Loadable Expandable Decorator Menu
  12. 12. S Examples: Loadable
  13. 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
  14. 14. LoadableBean: Loadable state bean LoadableBean is a simple POJO for specifying the state necessary to construct a Loadable: S A WebDriver reference S A load timeout
  15. 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. 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. 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.
  18. 18. public interface LoadableConfig { Class<? extends LoadableConfig> getType(); void setType(Class<? Extends LoadableConfig> type); Optional<Integer> getLoadTimeout(); void setLoadTimeout(Optional<Integer> timeout); }
  19. 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. 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. 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. 22. public class LoadableBeanImpl implements LoadableBean { private @Getter @Setter WebDriver driver; private @Getter @Setter int loadTimeout = DEFAULT_LOAD_TIMEOUT; }
  23. 23. LoadableConfigImpl: Default Loadable configuration bean S Uses Lombok to streamline the source code and reduce boilerplate code S Uses JSON as the external datastore
  24. 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. 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. 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…
  27. 27. …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…
  28. 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. 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. 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. 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. 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. 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. 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
  35. 35. public interface ConfigService { void setProfile(String profile); String getProfile(); <ConfigT extends LoadableConfig> ConfigT getConfig(String id); }
  36. 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. 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. 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…
  39. 39. …ConfigServiceImpl continued… public <ConfigT extends LoadableConfig> ConfigT getConfig(String id) { return (ConfigT)configs.get(id); } }
  40. 40. //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 } }
  41. 41. S Examples: Expandable
  42. 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. 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. 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. 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. 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); }
  47. 47. 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); }
  48. 48. 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(); }
  49. 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); }
  50. 50. 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); }
  51. 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. 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); }
  53. 53. 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); }
  54. 54. 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> { }
  55. 55. public interface Expandable extends Polleable { void expand(); void collapse(); boolean isExpanded(); boolean isCollapsed(); }
  56. 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. 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. 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. 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. 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…
  61. 61. …AbstractExpandable continued… public boolean isExpanded() { try { return getContainer().isDisplayed(); } catch(NoSuchElementException e) { return false; } } public boolean isCollapsed() { return !isExpanded(); } …continued…
  62. 62. …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…
  63. 63. …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…
  64. 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. 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…
  66. 66. …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…
  67. 67. …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)); } } }
  68. 68. 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…
  69. 69. …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 }
  70. 70. S Examples: Decorator
  71. 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. 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. 73. public class LoadableProvider<LoadableT extends Loadable> { private @Getter(value = AccessLevel.PROTECTED) LoadableT loadable; public LoadableProvider(LoadableT loadable) { this.loadable = loadable; } }
  74. 74. public interface ExpandableDecorator extends Expandable { LoadableProvider<? extends Expandable> getExpandableProvider(); default void expand() { getExpandableProvider() .getLoadable() .expand(); } default void collapse() { getExpandableProvider() .getLoadable() .collapse(); } …continued…
  75. 75. …ExpandableDecorator continued… default boolean isExpanded() { return getExpandableProvider() .getLoadable() .isExpanded(); } default boolean isCollapsed() { return getExpandableProvider() .getLoadable() .isCollapsed(); } default int getPollingTimeout() { return getExpandableProvider() .getLoadable() .getPollingTimeout(); } …continued…
  76. 76. …ExpandableDecorator continued… default int getPollingInterval() { return getExpandableProvider() .getLoadable() .getPollingInterval(); } }
  77. 77. S Examples: Menu
  78. 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. 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
  80. 80. 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); }
  81. 81. 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); }
  82. 82. 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> { }
  83. 83. 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…
  84. 84. …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…
  85. 85. …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…
  86. 86. …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. } }
  87. 87. 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…
  88. 88. …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…
  89. 89. …DropDownBasicMenu continued… protected void isLoaded() throws Error { try { expandableProvider.getLoadable().isLoaded(); } catch(NoSuchElementException e) { throw new Error(e)); } } }

×