Note
|
This is a rebranding of the html elements project. |
Next generation of the HtmlElements library is indeed a yet another WebDriver wrapper, but with a couple of fresh ideas:
-
Hierarchy of page objects for your application is now a very flexible composition of java interfaces.
-
A lot of additional control over elements behaviour is available via extension methods implemented in a fluent manner.
-
Actually, pretty much everything is extensible for your convenience: the library provides a baseline design for your project, allowing you to fine-tune the specifics.
Atlas has several basic entities that you will be working with:
WebPage - instances of this class will serve as top classes of your page objects hierarchy.
Atlas - returns WebPage instances that are wrapped around the given driver.
Atlas atlas = new Atlas();
SearchPage page = atlas.create(driver, SearchPage.class);
AtlasWebElement - is a parent interface for every web element you will be using in your tests.
ElementsCollection - an interface, which provides everything to work with a collection of AtlasWebElements.
1) Defining a page: pages are top-classes and they may contain elements or whole element blocks. @BaseUrl("http://www.base.url/search")
public interface SearchPage extends WebPage {
@FindBy("//form[contains(@class, 'search2 suggest2-form')]")
SearchArrow searchArrow();
@FindBy("//div[@class='toolbar']")
Toolbar toolbar();
@FindBy("//div[@class='user-tooltip']")
UserTooltip userTooltip();
} |
Web pages usually contain some high-level blocks, that we will neatly organize via a composition <body>
<div class='toolbar'>...</div>
<form class='search2 suggest2-form'>...</form>
...
</body> |
2) Organizing a block of web elements on the page: you need to extend an interface from AtlasWebElement and you are good to go public interface Toolbar extends AtlasWebElement {
@FindBy(".//span[@class = 'status-text']")
AtlasWebElement statusText();
} |
Your classes for such blocks of elements can contain sub-blocks, or once you’ve reached a singular element, use the AtlasWebElement as it’s type. <body>
<div class='toolbar'>
<span class='status-text'> Status </span>
...
</div>
<form class='search2 suggest2-form'>...</form>
...
</body> |
3) You can define a collection of elements for a page or for a block public interface Toolbar extends AtlasWebElement {
@FindBy(".//button[@class = 'action-button']")
ElementsCollection<ToolbarButton> buttons();
@FindBy(".//span[@class = 'status-text']")
AtlasWebElement statusText();
} |
Button elements have a specific ToolbarButton class to represent them. <body>
<div class='toolbar'>
<span class='status-text'> Status
</span>
<button class='action-button'>
<img src='//button-1-icon.png'>
...
</button>
<button class='action-button'>
<img src='//button-2-icon.png'>
...
</button>
...
</div>
<form class='search2 suggest2-form'>...</form>
...
</body> |
4) You can use parameterized selectors to simplify your work with homogeneous elements public interface SearchResultsPanel extends AtlasWebElement {
@Description("Search result for user {{userName}}")
@FindBy(".//div[contains(@class, 'search-result-item') and contains(., {{userName}}]")
UserResult user(@Param("userName") String userName);
} Invocation of parameterized method: UserResult foundUser = searchPage.resultsPanel().user("User 1"); |
Here parameterized element will match a block of elements for User 1 from search results. Parameters can be used with collections of elements as well. <body>
<div class='toolbar'>...</div>
<form class='search2 suggest2-form'>...</form>
<div class='search-results'>
<div class = 'search-result-item search-result-user'>
<span>User 1</span>
...
</div>
<div class = 'search-result-item search-result-task'>
<span>Task 1</span>
...
</div>
</div>
...
</body> |
5) Very often you will have some identical html blocks across different pages, e.g. toolbars, footers, dropdowns, pickers e.t.c. Once you wrote a class describing such a block, it can be easily reused across all of the pages: @BaseUrl("http://www.base.url/search")
public interface SearchPage extends WebPage, WithToolbar {
//other elements for search page here
}
@BaseUrl("http://www.base.url/account")
public interface AccountPage extends WebPage, WithToolbar {
//other elements for account page here
}
public interface WithToolbar extends AtlasWebElement {
@FindBy("//div[@class='toolbar']")
Toolbar toolbar();
} |
First page - base.url/search: <body>
<div class='toolbar'>...</div>
...
</body> Second page - base.url/account: <body>
<div class='toolbar'>...</div>
...
</body> both pages have a toolbar block, represented by the Toolbar class. |
After you have described web pages, it’s time to start organizing the logic of working with them. We prefer to use a BDD approach, where you break down all the interaction logic into simple actions and create one "step" method per action as it’s implementation. That is where all the extension methods from AtlasWebElement and ElementsCollection classes come in handy.
We will use harmcrest matchers for conditioning
Waiting on a condition for a single element.
AtlasWebElement
have two different extension methods for waiting.
First - waitUntil(Matcher matcher)
which waits with a configurable timeout and polling on some condition
in a passed matcher, throwing java.lang.RuntimeException
if condition has not been satisfied.
@Step("Make search with input string «{input}»")
public SearchPageSteps makeSearch(String input){
final SearchForm form = onSearchPage().searchPanel().form();
form.waitUntil(WebElement::isDisplayed) //waits for element to satisfy a condition
.sendKeys(input); //invokes standard WebElement's method
form.submit();
return this;
}
Second - should(Matcher matcher)
which waits the same way on a passed matcher, but throwing
java.lang.AssertionError
instead.
@Step("Check user «{userName}» is found")
public SearchPageSteps checkUserIsFound(String userName){
onSearchPage().resultsPanel().user(userName)
.should("User is not found", WebElement::isDisplayed);
//makes a waiting assertion here
return this;
}
Collections of elements are meant to be indiscrete objects. Working with individual elements of a collection
should generally be considered an anti-pattern, because elements behind the collection will not be refreshed
on the subsequent calls, and their usage may lead to the StaleElementReferenceException
.
Waiting and verifying collection state. ElementsCollection
has the same waitUntil()
and should()
methods as were
described above. Overall logic of their usage should be roughly the same, but with a little difference introduced by
abilities to filter via filter()
and to perform a mapping transformation via extract()
methods.
@Step("Check that search results contain exactly «{expectedItems}»")
public SearchPageSteps checkSearchResults(List<String> expectedItems){
onSearchPage().resultsForm().usersFound()
.waitUntil(not(empty()) //waiting for elements to load
.extract(user -> user.name().getText()) //extract AtlasWebElement to String
.should(containsInAnyOrder(expectedItems.toArray())); //assertion for a collection
}
Filtering
@Step("Check active users contain exactly «{expectedUsers}»")
public SearchPageSteps checkActiveUsersFound(List<String> expectedUsers){
onSearchPage().resultsForm().usersFound()
.waitUntil(not(empty()) //waiting for elements to load
.filter(user -> user.getAttribute("class").contains("selected"))
.extract(user -> user.name().getText()) //extract AtlasWebElement to String
.should(containsInAnyOrder(expectedItems.toArray())); //assertion for a collection
}
Don’t do this! Use a parameterized selector instead.
@Step("Select filter checkbox «{name}»")
public SearchPageSteps selectFilterCheckbox(String name){
onSearchPage().searchPanel().filtersTab().checkboxes()
.waitUntil(not(empty()))
.filter(chkbox -> chkbox.getText().contains(name))
.get(0).click(); //don't do this
}
WebPage
interface has several methods to help working with pages
Define a base url for page. If you annotate a WebPage
with @BaseUrl
you can specify an url to be opened
when WebPage
's open()
method is called.
@BaseUrl("http://www.base.url/search")
public interface SearchPage extends WebPage {
//elements for search page here
}
Then after instantiation you can call open()
method like this:
Atlas atlas = new Atlas();
SearchPage page = atlas.create(driver, SearchPage.class);
page.open()
Waiting for page loading. There is a special shouldUrl(Matcher<String> url)
that waits on the condition for page’s
current url and document.readyState
flag.
@BaseUrl("http://www.base.url/search")
public interface SearchPage extends WebPage {
@Description("Account button")
@FindBy("//div[@class = 'account-button']")
AtlasWebElement accountButton();
}
@BaseUrl("http://www.base.url/account")
public interface AccountPage extends WebPage {
//elements for account page here
}
To navigate between this two pages you have to wait for the account page to load after click on 'Account' button.
@Step("Go to the current account settings")
public void openAccountSettings() {
onSearchPage().accountButton().click();
onAccountPage().shouldUrl(equalTo("http://www.base.url/account"));
//continue working with account page
}