Page Objects is the recommended pattern to abstract the Selenium behaviour behind pages that specify the user interaction in more meaningful ways. The page objects is where the Selenium magic happens and this is where we need to commit to specific API, either Selenium API or WebDriver API.
The Pages object is typically a factory class that allows users to easily get hold of the objects for each web page, and can be injected into the JBehave Steps classes. The advantage of having a pages' factory class is that even as the number of page objects grow the dependencies at the Steps class level still remains the same. Typically, the implementation of a step requires interaction with more than one page.
The Pages object is injected with the Selenium API-specific objects:
public class Pages { private final Selenium selenium; private final ConditionRunner conditionRunner; private Home home; private FindSteps findSteps; // More pages as needed public Pages(Selenium selenium, ConditionRunner conditionRunner) { this .selenium = selenium; this .conditionRunner = conditionRunner; } public Home home() { if (home == null ) { home = new Home(selenium, conditionRunner); } return home; } public FindSteps findSteps() { if (findSteps == null ) { findSteps = new FindSteps(selenium, conditionRunner); } return findSteps; } } |
Each page extends the SeleniumPage, which provides a facade to the Selenium API. E.g.:
public class FindSteps extends AbstractPage { public FindSteps(Selenium selenium, ConditionRunner conditionRunner) { super (selenium, conditionRunner); } public void open() { clickLink( "Find Steps" ); waitForPageToLoad(); } public void pageIsShown() { found( "Patterns and methods matching the textual step" ); } public void find(String step) { type( "matchingStep" , step); clickButton( "Find" ); waitForPageToLoad(); } public void viewWithMethods() { selectByLabel( "viewSelect" , "WITH_METHODS" ); waitForPageToLoad(); } public void sortByPattern() { selectByLabel( "sortingSelect" , "BY_PATTERN" ); waitForPageToLoad(); } } |
Here AbstractPage is actually the class extending SeleniumPage and providing some methods common to all pages.
The Pages object is injected with the WebDriver API-specific objects:
public class Pages { private final WebDriverProvider driverProvider; private Home home; private FindSteps findSteps; // More pages as needed public Pages(WebDriverProvider driverProvider) { this .driverProvider = driverProvider; } public Home home(){ if ( home == null ){ home = new Home(driverProvider); } return home; } public FindSteps findSteps() { if ( findSteps == null ){ findSteps = new FindSteps(driverProvider); } return findSteps; } } |
Note that we inject a WebDriverProvider rather than a WebDriver instance. This allows the use to inject different strategies to provide WebDriver instances to the pages. For example, we have use a DefaultWebDriverProvider, which provides vanilla FirefoxWebDrivers, or PropertyWebDriverProvider, which provides WebDriver instances based on the system property browser, which must match (case-insensitively) one of the values of Browser enum.
Each page extends the WebDriverPage, which provides a facade to the WebDriver API. E.g.:
public class FindSteps extends AbstractPage { public FindSteps(WebDriverProvider driverProvider) { super (driverProvider); } public void open(){ findElement(By.linkText( "Find Steps" )).click(); } public void pageIsShown() { found( "Patterns and methods matching the textual step" ); } public void find(String step) { findElement(By.name( "matchingStep" )).sendKeys(step); findElement(By.name( "findButton" )).click(); } public void viewWithMethods() { new Select(findElement(By.name( "viewSelect" ))).selectByVisibleText( "WITH_METHODS" ); } public void sortByPattern() { new Select(findElement(By.name( "sortingSelect" ))).selectByVisibleText( "BY_PATTERN" ); } } |
Here again AbstractPage is actually the class extending WebDriverPage and providing some methods common to all pages.