Skip to content

Latest commit

 

History

History
353 lines (297 loc) · 10.3 KB

README.adoc

File metadata and controls

353 lines (297 loc) · 10.3 KB
Note
This is a rebranding of the html elements project.

Atlas

Next generation of the HtmlElements library is indeed a yet another WebDriver wrapper, but with a couple of fresh ideas:

  1. Hierarchy of page objects for your application is now a very flexible composition of java interfaces.

  2. A lot of additional control over elements behaviour is available via extension methods implemented in a fluent manner.

  3. 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.

Overview

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.

Structuring Page Objects

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

Features

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

Working with elements

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

Working with collections

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
    }

Working with pages

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