Friday, June 6, 2014

form element - select

Select is a dropdown user interface for people to select the desired the value from the list. for example,



Selenium UI Support provided a Select class which is great for users to manipulate select element on html page, however, it lacks a feature which will cause problem on AJAX enabled web pages. If the option list is built dynamically, selecting before it is populated will result in NoSuchElementException. Thus it requires adding waiting mechanism to the select before selecting an option from it. This waiting logic is encapsulated into a Select class in Selenium Capsules. The Select class takes a SelectLocator as parameter.

public class SelectLocator<Where extends Searchable<Where>>
        extends Locators<Where, Select> {

    private static final Logger log = getLogger(SelectLocator.class);

    public SelectLocator(Supplier<By> selector) {
        super((Where where) -> {
            final Element element = where.untilFound(selector);
            try {
                element.until(Locators.<Element>elements(OPTION).and(new HasElements<>()));
                return new Select(element);
            } catch (NoSuchElementException e) {
                element.click();
                where.save();
                log.error("Timeout waiting for the option list to populate.", e);
                throw e;
            }
        });
    }
}


What it does is to find the Select element first, then wait for option list becomes populated and use it as a parameter for the constructor of the Select class in Selenium Capsules.

public class Select<Where extends Searchable<Where>> {

    private static final Logger log = getLogger(Select.class);

    private final Where where;
    private final SelectLocator<Where> locator;

    /**
     * Constructor of the Select, It is a wrapper for the Select from Selenium UI.
     * @param where
     * @param locator
     */
    public Select(Where where, SelectLocator<Where> locator) {
        this.where = where;
        this.locator = locator;
    }

    public void selectByVisibleText(Object text) {
        log.info("selecting select[" + locator + "] using [" + text + "]");
        locator.locate(where).selectByVisibleText(text.toString());
    }
}


Unlike the selectByVisibleText in the Selenium UI Select class which takes String as parameter, this method in Selenium Capsules takes Object as parameter, so anything can be used as parameter, which make it possible to use enum, as long as the toString method of the enum to a string that can be found in the option list, then it can be used

public enum CreditCardType {

    American_Express,
    JCB,
    MasterCard,
    Visa,
    Discover;

    @Override
    public String toString() {
        return REPLACE_UNDERSCORE.locate(this.name());
    }

    public static CreditCardType fromString(String string) {
        return valueOf(RESTORE_UNDERSCORE.locate(string));
    }
}


And we add a method to FormControl as following,
    /**
     * Select the dropdown by given value.
     *
     * @param selector selector
     * @param value    value
     */
    @SuppressWarnings("unchecked")
    default public void select(Supplier<By> selector, Object value) {
        new Select<>((Where) this, Locators.<Where>select(selector)).selectByVisibleText(value);
    }


Then we can use this method to select the dropdown.

        select(CARD_TYPE, CreditCardType.MasterCard);


If you don't want to use selectByVisibleText method you can simply create a Select object and use the other methods, all methods in Select class from selenium ui support are still available through the Select in Selenium Capsules, with some modification and additions, 1. parameter type from String to Object to enable enum as parameter, 2. additional method to accept enum as parameter and use its ordinal,
    public void selectByEnumOrdinal(Enum index) {
        locate().selectByIndex(index.ordinal());
    }
    
    public void selectByIndex(int index) {
        locate().selectByIndex(index);
    }



And CARD_TYPE is an enum instance to provide a Selenium By.id("card-type"), where Supplier is an interface since Jave 8.
public enum BookStoreId implements Supplier<By> {

    CARD_TYPE("card-type");

    private final By by;

    private BookStoreId(String id) {
        this.by = id(id);
    }

    @Override
    public By get() {
        return by;
    }

    @Override
    public String toString() {
        return by.toString();
    }
}


form element - checkbox

Checkboxes are one kind of form elements allowing user to answer true or false questions. It provides toggle behaviour when user clicks tone checkbox. Here are two checkboxes on a form,

Based on the common requirement of using a checkbox when doing test automation, we can define a Checkbox class to encapsulate the behaviours as following, and it only has two public methods, setValue and isChecked.

public class Checkbox<Where extends Searchable<Where>> {

    private final Where where;
    private final Locator<Where, Element> locator;

    /**
     * Constructor of the checkbox.
     *
     * @param where    the place the checkbox can be found
     * @param selector the selector that leads to the checkbox
     */
    Checkbox(final Where where, Supplier<By> selector) {
        this.where = where;
        this.locator = element(selector);
    }

    /**
     * Change the checkbox according to the value parameter
     *
     * @param value true or false
     */
    public void setValue(boolean value) {
        Element apply = locator.locate(where);
        if (apply != null && apply.isSelected() != value) {
            apply.click();
        }
    }

    /**
     * @return whether the checkbox is checked or not
     */
    public boolean isChecked() {
        return locator.and(CHECKED).and(TRUE).test(where);
    }
}


If you don't want to create a new instance of Checkbox, you can use the helper methods in FormControl to read and write the checkbox that can be found using the locator parameter,

    /**
     * Check if the checkbox is checked by the given selector.
     *
     * @param selector selector
     * @return true if it is checked.
     */
    @SuppressWarnings("unchecked")
    default public boolean isChecked(Supplier<By> selector) {
        return new Checkbox<>((Where) this, selector).isChecked();
    }

    /**
     * Set checkbox to the given value.
     *
     * @param selector selector
     * @param value    value
     */
    @SuppressWarnings("unchecked")
    default public void check(Supplier<By> selector, boolean value) {
        new Checkbox<>((Where) this, selector).setValue(value);
    }


So in a page,

        AbstractPage page = new AbtractPage(Browsers.CHROME);
        System.out.println(page.isChecked(CONFIRM_EMAIL_)),
        System.out.println(page.isChecked(RATINGS_______)),

        page.check(CONFIRM_EMAIL_, false);
        page.check(RATINGS_______, true);


And it will print true and false on the console and change the checkboxes to the following,



Here is the code how it is used in a form and a page object and a test,

public class OtherInformationForm extends AbstractPage {

    public OtherInformationForm(AbstractPage page) {
        super(page);
    }

    public void setOtherInformation(OtherInformation info) {
        put(BILLING_EMAIL___, info.emailAddress);
        put(COMMENTS________, info.comments);
        check(CONFIRM_EMAIL_, info.confirmEmail);
        check(RATINGS_______, info.askRating);
        radio(MAILING_OPTION, info.mailingOptions);
    }

    public OtherInformation getOtherInformation() {
        return new OtherInformation(
                get(BILLING_EMAIL___),
                isChecked(CONFIRM_EMAIL_),
                isChecked(RATINGS_______),
                MailingOptions.valueOf(get(MAILING_OPTION)),
                get(COMMENTS________));
    }

}

public class ShoppingCartPage extends AbstractPage {

    private final BillingAddressForm billingAddressForm = new BillingAddressForm(this);
    private final CreditCardForm creditCardForm = new CreditCardForm(this);
    private final OtherInformationForm otherInformationForm = new OtherInformationForm(this);

    public ShoppingCartPage(AbstractPage page) {
        super(page);
    }

    public void setBillingAddress(Address address) {
        billingAddressForm.setBillingAddress(address);
    }

    public void getBillingAddress() {
        billingAddressForm.getBillingAddress();
    }

    public void setCreditCard(CreditCard card) {
        creditCardForm.setCreditCard(card);
    }

    public CreditCard getCreditCard() {
        return creditCardForm.getCreditCard();
    }

    public void setQuantity(int quantity) {
        put(Xpath.QUANTITY, quantity);
        button(UPDATE).click();
    }

    public void setOtherInformation(OtherInformation info) {
        otherInformationForm.setOtherInformation(info);
    }

    public OtherInformation getOtherInformation() {
        return otherInformationForm.getOtherInformation();
    }

    public void continues() {
        button(CONTINUE).click();
    }

    public ErrorMessages getErrorMessages() {
         return new ErrorMessages(this);
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:bookstore/beans/context.xml"})
public class BookStoreShoppingTest {

    @Autowired
    private Address billingAddress;

    @Autowired
    private CreditCard creditCard;

    @Autowired
    private OtherInformation otherInformation;

    @Autowired
    private ErrorMessages expectedErrorMessages;

    @Autowired
    private BookStoreHomePage homePage;

    @Test
    public void invalidCardInfo() {

        BookListPage listPage = new BookListPage(homePage, homePage.link(JAVA), IS_COPYRIGHTED) {{
            open();
            link(ACTIVE_MQ_IN_ACTION).click();
        }};
        BookDetailsPage bookPage = new BookDetailsPage(listPage) {{
            until(IS_COPYRIGHTED);
            secondAddToCart().click();
        }};

        ShoppingCartPage cartPage = new ShoppingCartPage(bookPage) {{
            setQuantity(2);
            setBillingAddress(billingAddress);
            setCreditCard(creditCard);
            setOtherInformation(otherInformation);
            continues();
        }};

        assertEquals(expectedErrorMessages, cartPage.getErrorMessages());
    }

    @Test
    public void invalidCardInfoNormalWay() {

        BookListPage listPage = new BookListPage(homePage, homePage.link(JAVA), IS_COPYRIGHTED);
        listPage.open();
        listPage.link(ACTIVE_MQ_IN_ACTION).click();

        BookDetailsPage bookPage = new BookDetailsPage(listPage);
        bookPage.until(IS_COPYRIGHTED);
        bookPage.secondAddToCart().click();

        ShoppingCartPage cartPage = new ShoppingCartPage(bookPage);
        cartPage.setQuantity(2);
        cartPage.setBillingAddress(billingAddress);
        cartPage.setCreditCard(creditCard);
        cartPage.setOtherInformation(otherInformation);
        cartPage.continues();

        assertEquals(expectedErrorMessages, cartPage.getErrorMessages());
    }


    @Before
    public void setup() {
        homePage.open();
    }

    @After
    public void close() {
        homePage.close();
    }

}


Thursday, June 5, 2014

form element - radio button

Radio buttons are one type of graphical user interface element that allows the user to choose only one of the available options.



And this is the HTML code behind it,



You can see they have the same name,

name="customFieldDS.customfield_ROW0_value"

this is an important character of radio buttons, they all share the same name and that's how browser knows to switch to the answer you just clicked. We are going to use this feature to locate the radio button group and select the one clicked by user. For this mailing option radio group, we can define a Name selector enum instance for it,

import org.openqa.selenium.By;

import java.util.function.Supplier;

/**
 * This enum has elements with ByName from Selenium By API.
 */
public enum Name implements Supplier<By> {

    Q("q"),
    MAILING_OPTION("customFieldDS.customfield_ROW0_value"),
    QUANTITY("cartDS.shoppingcart_ROW0_m_orderItemVector_ROW0_m_quantity");

    private final By by;

    private Name(String id) {
        this.by = By.name(id);
    }

    /**
     * @return the by instance variable which is a ByName.
     */
    @Override
    public By get() {
        return by;
    }

    @Override
    public String toString() {
        return by.toString();
    }
}


To find all elements with the same name, we need to use the findElements method of WebDriver, since Selenium Capsules encapsulate Selenium API, the class to use is,

import com.algocrafts.pages.Element;
import com.algocrafts.pages.Locators;
import com.algocrafts.selenium.Searchable;
import org.openqa.selenium.By;

import java.util.function.Supplier;
import java.util.stream.Stream;

public class ElementsLocator<Where extends Searchable<Where>>
        extends Locators<Where, Stream<Element>> {

    public ElementsLocator(Supplier<By> selector) {
        super((Where where)
                        -> where.findElements(selector)
        );
    }
}


ElementsLocator extends a base locator called Locators and what it does is to pass a lambda expression to its super class. The lambda expression represents a Locator which calls the findElements of a Where generic parameter type which can be an AbstractPage, an Element or an AbstractForm.





This radioButtonGroup Locator is used to locate all the radio buttons into a Stream of Elements, if you don't understand what Stream is, you can think of it is a List.

And we also defined an enum for the values of the radio buttons



So on the ShoppingCartPage, we can also this method to select the radio, what it means is select the MailingOptions.No_Promotional_Mailers value from the radio group which can be located by Selector Name.MAILING_OPTION,

        cartPage.radio(Name.MAILING_OPTION, MailingOptions.No_Promotional_Mailers);


By the way, the following code works the same way as the code above, but the code above is much cleaner, that's the power of enum.

   cartPage.radio(() -> By.name("customFieldDS.customfield_ROW0_value"), 
                  "No promotional mailers. I will still receive updates on my MEAPs and other books.");


And here is the code for the radio method above, what it does is to create an instance of RadioButton class with parameter from where it is called, since it is called from cartPage, (Where) this refers to the page, and selector which is Name.MAILING_OPTION, and then call the setValue method of the RadioButton instance.

    /**
     * Choose the radio by given option.
     *
     * @param selector selector
     * @param option   option
     */
    @SuppressWarnings("unchecked")
    default public void setRadio(Supplier<By> selector, Object option) {
        new RadioButton<>((Where) this, selector).setValue(option);
    }


Here is a test setting the value of a radio and read it back,

public class RadioTest {
    @Test
    public void testRadio() {
        Browser browser = Browsers.CHROME;
        browser.get("http://localhost:63342/seleniumcapsules/html/radio.html");
        Page page = new Page(browser);
        page.setRadio(MAILING_OPTION, No_Promotional_Mailers);

        assertEquals(No_Promotional_Mailers, fromString(page.getRadio(MAILING_OPTION)));
        assertEquals(No_Promotional_Mailers, from(page.getRadio(MAILING_OPTION)));
        assertEquals(No_Promotional_Mailers, page.getRadio(MAILING_OPTION, (s) -> from(s)));
        assertEquals(No_Promotional_Mailers, page.getRadio(MAILING_OPTION, MailingOptions::from));
    }
}


public enum MailingOptions {

    Weekly_Newsletter("Weekly newsletter--New books, updates, news, and special offers"),
    Deal_Of_the_Day("Deal of the Day--These amazing special offers last just 24 hours!"),
    Both("Both"),
    No_Promotional_Mailers("No promotional mailers. I will still receive updates on my MEAPs and other books."),
    Keep_Me("Keep me on the lists I'm already on.");

    private final String string;

    private MailingOptions(String string) {
        this.string = string;
        MapHolder.map.put(string, this);
    }

    @Override
    public String toString() {
        return string;
    }

    /**
     * This method filtering the enum constants using the string and return the first one.
     *
     * @param string the string value
     * @return enum with the string value
     */
    public static MailingOptions fromString(String string) {
        return of(values()).filter((o) -> string.equals(o.string)).findFirst().get();
    }

    /**
     * This method look up the enum constant in the map.
     * @param string
     * @return
     */
    public static MailingOptions from(String string) {
        return MapHolder.map.get(string);
    }

    private static class MapHolder {
        private static final Map<String, MailingOptions> map = newHashMap();
    }
}


There are some subtle difference between the 4 assert statements,
        //This line converts the String to MailingOptions using fromString method.
        assertEquals(No_Promotional_Mailers, fromString(page.getRadio(MAILING_OPTION)));

        //This line converts the string to MailingOptions using the from method.
        assertEquals(No_Promotional_Mailers, from(page.getRadio(MAILING_OPTION)));

        //This line calls the getRadio method and passes in a lambda expression which converts the string to enum.
        assertEquals(No_Promotional_Mailers, page.getRadio(MAILING_OPTION, (s) -> from(s)));

        //This line calls the getRadio method and passes in a method reference of MailOptions.from(String string).
        assertEquals(No_Promotional_Mailers, page.getRadio(MAILING_OPTION, MailingOptions::from));


You can see Selenium Capsules framework uses enum extensively to increase the readability of the code, for comparison purpose, here is the code without using framework,
//This is an ugly test not using page framework, it has the same function as the test above. :(
@Test
public void testRadio() {
    System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
    WebDriver webDriver = new ChromeDriver();
    webDriver.get("http://localhost:63342/seleniumcapsules/html/radio.html");
    List<WebElement> radios = webDriver.findElements(By.name("customFieldDS.customfield_ROW0_value"));
    for (WebElement radio : radios) {
       if (radio.getAttribute("value").equals("No promotional mailers. I will still receive updates on my MEAPs and other books.")) {
          radio.click();
       }
    }

    radios = webDriver.findElements(By.name("customFieldDS.customfield_ROW0_value"));
    for (WebElement radio : radios) {
       if (radio.getAttribute("checked").equals("true")) {
          assertEquals("No promotional mailers. I will still receive updates on my MEAPs and other books.", radio.getAttribute("value"));
       }
    }
}

and code using the framework,
@Test
public void testRadio() {
    Browser browser = Browsers.CHROME;
    browser.get("http://localhost:63342/seleniumcapsules/html/radio.html");
    Page page = new Page(browser);
    page.setRadio(MAILING_OPTION, No_Promotional_Mailers);
    assertEquals(No_Promotional_Mailers, page.getRadio(MAILING_OPTION, MailingOptions::from));
}

form element - input

Quite often, test automation requires to fill out forms and click button or link to send data back to server, thus it is necessary to introduce some Classes to be responsible to do this repeated tasks such findElement, sendKeys etc.

Inputs are most common form elements for user to enter information, for a online shopping site, shipping information and billing information are required for any transaction,



On this form, most fields are inputs, and here is how to enter text on a single field,

        // Find the text input element by its name
        WebElement element = driver.findElement(By.name("q"));

        // Enter something to search for
        element.sendKeys("Cheese!");

        // Now submit the form. WebDriver will find the form for us from the element
        element.submit();


And it doesn't consider the possibility that it may require some waiting before you can find the element and send keys to it, all things considered, it could be very complex.
So it is necessary to introduce this method to combine all those small steps and provide and single transation like,
        put(()->By.name("q"), "Cheese!");


So we can design an Input class to encapsulate the behaviour of read, write and autocomplete on html text input fields,

public class Input<Where extends Searchable<Where>> implements Supplier<String> {



    /**
     * the value of input field, for example, "good" will be return
     * <p>
     * String value = page.get(() -> By.name("status"))
     * <p>
     * <input name="status" value="good"/>
     *
     * @return the value of the input
     */
    public String get() {
         ...
    }

    /**
     * set the value of input field, for example,
     * <p>
     * after,
     * page.set(() -> By.name("status"), "good");
     * <p>
     * it will be,
     * <input name="status" value="good"/>
     *
     * @param value the value to set
     */

    public void put(final Object value) {
        ...
    }

    /**
     * Test the autocomplete function for the input by given selector, click the element
     * on the suggestion list which has the same value of value parameter.
     * <p>
     * Please refer "http://seleniumcapsules.blogspot.com/2014/05/by-xpath.html"
     *
     * @param value   value
     * @param locator locator
     */
    public void autocomplete(Object value, Locator<Where, Element> locator) {
        ...
    }

}



public interface FormControl<Where extends Searchable<Where>> {

    /**
     * Check if the checkbox is checked by the given selector.
     *
     * @param selector selector
     * @return true if it is checked.
     */
    @SuppressWarnings("unchecked")
    default public boolean isChecked(Supplier<By> selector) {
        return new Checkbox<>((Where) this, selector).isChecked();
    }

    /**
     * Set checkbox to the given value.
     *
     * @param selector selector
     * @param value    value
     */
    @SuppressWarnings("unchecked")
    default public void check(Supplier<By> selector, boolean value) {
        new Checkbox<>((Where) this, selector).setValue(value);
    }

    /**
     * Read the value of the radio by given option.
     *
     * @param selector selector
     * @return the value of selected radio.
     */
    @SuppressWarnings("unchecked")
    default public String radio(Supplier<By> selector) {
        return new RadioButton<>((Where) this, selector).get();
    }

    /**
     * Choose the radio by given option.
     *
     * @param selector selector
     * @param option   option
     */
    @SuppressWarnings("unchecked")
    default public void radio(Supplier<By> selector, Object option) {
        new RadioButton<>((Where) this, selector).setValue(option);
    }

    /**
     * Select the dropdown by given value.
     *
     * @param selector selector
     * @param value    value
     */
    @SuppressWarnings("unchecked")
    default public void select(Supplier<By> selector, Object value) {
        new Selection<>((Where) this, Locators.<Where>select(selector)).selectByVisibleText(value);
    }

    /**
     * Read value from an input field.
     *
     * @param selector selector
     * @return its value.
     */
    @SuppressWarnings("unchecked")
    default public String get(Supplier<By> selector) {
        return new Input<>((Where) this, selector).get();
    }

    /**
     * Enter text into an input field.
     *
     * @param selector selector
     * @param value    value
     */
    @SuppressWarnings("unchecked")
    default public void put(Supplier<By> selector, Object value) {
        new Input<>((Where) this, selector).put(value);
    }

    /**
     * Autocomplete for text field and return the first found suggestion match the whole word.
     *
     * @param selector selector
     * @param value    value
     * @param locator  locator
     */
    @SuppressWarnings("unchecked")
    default public void autocomplete(Supplier<By> selector, Object value, Locator<Where, Element> locator) {
        new Input<>((Where) this, selector).autocomplete(value, locator);
    }
}
So for that form, it can be just like this,

package com.bookstore;

import com.algocrafts.domain.Countries;
import com.algocrafts.domain.UnitedStates;
import com.algocrafts.pages.AbstractPage;
import com.bookstore.domain.Address;

import static com.bookstore.BookStoreId.*;

public class BillingAddressForm extends AbstractPage {
    public BillingAddressForm(AbstractPage page) {
        super(page);
    }

    public void setBillingAddress(Address address) {
        put(BILLING_FIRST_NAME, address.firstName);
        put(BILLING_LAST_NAME_, address.lastName);
        put(BILLING_ADDRESS1__, address.street1);
        put(BILLING_ADDRESS2__, address.street2);
        put(BILLING_CITY______, address.city);
        put(BILLING_STATE_____, address.state);
        put(BILLING_ZIP_______, address.zipcode);
        select(BILLING_COUNTRY___, address.country);
    }

    public Address getBillingAddress() {
        return new Address(
                get(BILLING_ADDRESS1__),
                get(BILLING_ADDRESS2__),
                get(BILLING_CITY______),
                get(BILLING_ZIP_______),
                UnitedStates.fromString(get(BILLING_STATE_____)),
                Countries.fromString(get(BILLING_COUNTRY___)),
                get(BILLING_FIRST_NAME),
                get(BILLING_LAST_NAME_));
    }

}


Input class and helpers in FormControl only give you most frequently used functions of the text input, reading its value and setting a value to it, if other functions of the input are needed for tests, please use Element instead, Element implement WebElement so it provides the same functionality as WebElement. It also have additional functions such as untilFound method which explicitly wait for the element to be found, this untilFound method is available to both Element and AbstractPage since it is a default method in Searchable interface, it handles explicit wait in a way similar to implicit wait which saves a lot of code to handle explicit wait.


    /**
     *  Find the first element until timeout then throw NoSuchElementException 
     *
     * @param by selector
     * @return the first element or throw NoSuchElementException
     */
    default public Element untilFound(Supplier<By> by) {
        return until((Where page) -> new Element(findElement(by.get())));
    }

Wednesday, June 4, 2014

what inside selenium capsules?

Selenium Capsules Selenium WebDriver
Searchable SearchContext
ExplicitWait FluentWait
Browser WebDriver
Element WebElement
Supplier By

Tuesday, June 3, 2014

specification

In this post, table manners, I delibrately gave an incomplete test, just to show the way to read a table, now, I am going to talk another important topic, where to assert?

To organize assertion into a cohesive group and output meaningful difference message for debugging purpose, I defined a TableContents class to measure the actual table and put the differences inside a SetDiff class, it means the difference of two sets.




We need to add hashCode method in the Person class since it is used in a HashSet now,



SetDiff class uses Set Operation to find out what elements are only in expectation and what are only in actual results,


S1 - expectation, S2 - actual
only in expectation = S1 - S2
only in actual = S2 - S1

The test is complete.



By splitting responsibilities among different classes, the test becomes very clean, and the error messages can show what's the difference in expectation and actual results. For this test, let us pretend last time, there was only one row in the table,



So the expectation used to be,


Now you are making changes and added 3 rows of data, and changed first row from 49 to 50, when you run the test, here is the result,



It tells you what's the actual result, what is in expectation but doesn't show in actual result and the unexpected result in actual, best thing yet, you can just copy the actual result into the expectation after you verify it is the right result.

And test passes,


However, there is no assertion method in junit which takes Object as parameter, I wrote one by myself, but I am asking Kent Beck to add this into future release of junit.

Monday, June 2, 2014

table manners for selenium capsules

Here is a simple table, http://www.w3schools.com/html/html_tables.asp


Let us see how Selenium Capsules can help to read the content of it.



The Table class only has two methods now, getHeader and getRows, Stream is a new collection type appears in Java 8.

And here is the test to read the table,


You can see, the table supports type, it will return the type in a Stream. For this table, we define a simple domain class called Person,

    class Person {
        private final String firstName;
        private final String lastName;
        private final int points;

        Person(String firstName, String lastName, int points) {

            this.firstName = firstName;
            this.lastName = lastName;
            this.points = points;
        }

        @Override
        public String toString() {
            return firstName + "|" + lastName + "|" + points;
        }
    }


When we construct the Table, we need to pass a Locator to locate the table and a Mapper to map the contents in a row to the domain class,

        Locator<AbstractPage, Element> locator = Locators.<AbstractPage>element(MAIN).and(element(TABLE));

        Locator<Stream<Element>, Person> mapper = (stream) -> {
            Iterator<String> iterator = stream.map(TEXT).iterator();
            return new Person(iterator.next(), iterator.next(), PARSE_INT.locate(iterator.next()));
        };


The mapper will read all the TD tags in the TR tags and call the new Person for each TR tag.
<tr>
  <td>Jill</td>
  <td>Smith</td> 
  <td>50</td>
</tr>
            return new Person("Jill", "Smith", 50);


it prints this when you run it

Firstname|Lastname|Points|
Jill|Smith|50
Eve|Jackson|94
John|Doe|80
Adam|Johnson|67
The following code is called Method Reference in Java 8.

        table.getRows().forEach(
                System.out::println
        );


It has good table manners.