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:
WebSite - instances of this class will serve as top classes of your page objects hierarchy.
WebPage - instances of this class will contain a block of web elements.
Atlas - returns WebSite instances that are wrapped around the given driver.
Atlas atlas = new Atlas();
BaseSite site = atlas.create(driver, BaseSite.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 site: site is top-interface and it contains pages your web application public interface BaseSite extends WebSite {
@Page
MainPage mainPage();
@Page("/search")
SearchPage searchPage();
@Page("/account")
AccountPage accountPage();
} |
2) Defining a page: pages may contain elements or whole element blocks. 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> |
3) 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> |
4) 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> |
5) 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> |
6) 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: public interface SearchPage extends WebPage, WithToolbar {
//other elements for search page here
}
public interface AccountPage extends WebPage, WithToolbar {
//other elements for account page here
}
public interface WithToolbar extends AtlasWebElement {
@FindBy("//div[@class='toolbar']")
Toolbar toolbar();
} |
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(Matchers.is(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", Matchers.is(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
}
There are different ways to set BASE_URL
for page
1) Forward parameter url
in open
Webpage method
@Test
public void searchTest() {
onSearchPage().open("http://baseurl.com/search");
}
2) Define a base url for site in configuration
@BeforeTest
public void init() {
Atlas atlas = new Atlas(WebDriverConfiguration(driver, "base.url"));
}
Then if you annotate a WebPage
with @Page
you can specify an url to be opened
when WebPage
's open()
method is called.
public interface BaseSite extends WebSite {
@Page("/search")
SearchPage searchPage();
//elements for search page here
}
Then after instantiation you can call open()
method like this:
Atlas atlas = new Atlas(WebDriverConfiguration(driver, "http://base.url"));
BaseSite site = atlas.create(driver, BaseSite.class);
site.searchPage().open(); //go to http://base.url/search