Saturday, June 14, 2014

handle ajax

When testing web sites built with AJAX technology, special cares must be taken to make sure when you call webDriver.findElement. Here is a web site selling event tickets, Ticketfly, to test the change location function, you may be attempted to write a test like this,

    @Test
    public void changeLocationUsingSelenium() {
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
        WebDriver webDriver = new ChromeDriver();
        webDriver.get("http://www.ticketfly.com");
        webDriver.findElement(linkText("change location")).click();
        webDriver.findElement(linkText("CANADA")).click();
        webDriver.findElement(linkText("All Canada")).click();
        assertEquals("Canada", 
                     webDriver.findElement(By.className("tools-location"))
                              .findElement(By.tagName("a"))
                              .findElement(By.tagName("strong"))
                              .getText());
    }


Unfortunately, this test doesn't work, you will get this exception when you try to run the test,

Starting ChromeDriver (v2.10.267517) on port 39666
Only local connections are allowed.
log4j:WARN No appenders could be found for logger (org.apache.http.client.protocol.RequestAddCookies).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

org.openqa.selenium.NoSuchElementException: no such element
  (Session info: chrome=35.0.1916.153)
  (Driver info: chromedriver=2.10.267517,platform=Mac OS X 10.9.3 x86_64) (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 115 milliseconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/no_such_element.html
Build info: version: '2.42.2', revision: '6a6995d31c7c56c340d6f45a76976d43506cd6cc', time: '2014-06-03 10:52:47'
System info: host: 'yujun.home', ip: '192.168.1.2', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.9.3', java.version: '1.8.0_05'
Driver info: org.openqa.selenium.chrome.ChromeDriver
Capabilities [{applicationCacheEnabled=false, rotatable=false, chrome={userDataDir=/var/folders/ks/4h1b7nps1vx5712vz12qd3880000gn/T/.org.chromium.Chromium.WmhgSi}, takesHeapSnapshot=true, databaseEnabled=false, handlesAlerts=true, version=35.0.1916.153, platform=MAC, browserConnectionEnabled=false, nativeEvents=true, acceptSslCerts=true, locationContextEnabled=true, webStorageEnabled=true, browserName=chrome, takesScreenshot=true, javascriptEnabled=true, cssSelectorsEnabled=true}]
Session ID: e2f0a757e78c351f2808a6b957c534c5
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
 at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:204)
 at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:156)
 at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:599)
 at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:352)
 at org.openqa.selenium.remote.RemoteWebDriver.findElementByLinkText(RemoteWebDriver.java:401)
 at org.openqa.selenium.By$ByLinkText.findElement(By.java:242)
 at org.openqa.selenium.remote.RemoteWebDriver.findElement(RemoteWebDriver.java:344)
 at com.algocrafts.TicketflyTest.changeLocationUsingSelenium(TicketflyTest.java:26)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.runners.BlockJUnit4ClassRunner.runNotIgnored(BlockJUnit4ClassRunner.java:79)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:71)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
 at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
 at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:211)
 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:67)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)


Process finished with exit code 255



The reason is this function is built upon AJAX, it doesn't refresh the entire page, it just repaint the specific area with the new DOM elements. In order to test this feature, we need to add Explicit Wait mechanism to make sure the elements we are looking for appear after certain waiting period, the suitable classes from Selenium are WebDriverWait and FluentWait, using either one, we can rewrite the about test as following,

    @Test
    public void changeLocationUsingSeleniumWithExplicitWait() {
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
        WebDriver webDriver = new ChromeDriver();
        webDriver.get("http://www.ticketfly.com");
        webDriver.findElement(linkText("change location")).click();
        WebDriverWait wait = new WebDriverWait(webDriver, 5);
        WebElement canada = wait.until(new Function<WebDriver, WebElement>() {
            @Override
            public WebElement apply(WebDriver webDriver) {
                return webDriver.findElement(linkText("CANADA"));
            }
        });
        canada.click();
        WebElement allCanada = wait.until(new Function<WebDriver, WebElement>() {
            @Override
            public WebElement apply(WebDriver webDriver) {
                return webDriver.findElement(linkText("All Canada"));
            }
        });
        allCanada.click();
        assertEquals("Canada", 
                     webDriver.findElement(By.className("tools-location"))
                              .findElement(By.tagName("a"))
                              .findElement(By.tagName("strong"))
                              .getText());
    }



When you run it, it passes.

This test become very verbose when we try to address some common concerns such as wait, it repeats many similar but not exactly same codes. It takes longer time for other developer to read the code and understand what it is doing. We can use framework approach to clean up the code, let us compare the following test written using Selenium Capsules,

    @Test
    public void changeLocationUsingBrowser() {
        Browser browser = CHROME;
        browser.get("http://www.ticketfly.com");
        browser.link(CHANGE_LOCATION).click();
        browser.link(CANADA).click();
        browser.link(ALL_CANADA).click();

        assertEquals("Canada",  Locators.<AbstractPage>element(TOOLS_LOCATION)
                .and(element(A))
                .and(element(STRONG))
                .and(TEXT).locate(new Page(browser)));
    }


It is much cleaner if you write code using framework approach since framework handles many tedious common concerns such as wait and see.

The test can be further cleaned by introducing a Page Object,

    @Test
    public void changeLocation() {
        TicketflyHomePage page = new TicketflyHomePage(CHROME);
        page.open();
        page.changeLocation(CANADA, ALL_CANADA);

        assertEquals("Canada", page.currentLocation());
    }


Even I am impressed by my own work. In case you wonder which class handles the Explicit Wait, it is the ElementLocator class in Selenium Capsules, it calls the untilFound method of Searchable interface as described here.

public class ElementLocator<Where extends Searchable<Where>>
        extends Locators<Where, Element> {

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


Unlike the original findElement method in SearchContext of the Selenium API, two new methods are introduced to handle different situations,
  • 1. If an element may appear or may not, use tryElement, it returns null if not found.
  • 2. If an element will appear after sometime since it is AJAX managed DOM element, use untilFound method which is explicitly waiting for the element to appear.


  •     /**
         * Find the first element or return null if nothing found.
         *
         * @param by selector
         * @return the first element or return null if nothing found.
         */
        default public Element tryElement(Supplier<By> by) {
            try {
                return findElement(by.get());
            } catch (NoSuchElementException e) {
                return null;
            }
        }
    
        /**
         * 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) -> findElement(by.get()));
        }
    

    Thursday, June 12, 2014

    handle alert and confirm

    The following two tests demonstrate how to click "OK" on a confirm message box, the first method uses Selenium directly, the second one uses Selenium Capsules framework,

    
    public class ConfirmTest {
    
        @Test
        public void clickConfirmUsingSelenium() {
            System.setProperty("webdriver.chrome.driver", "src/main/resources/chrome/chromedriver");
            WebDriver webDriver = new ChromeDriver();
            webDriver.get("http://localhost:63342/seleniumcapsules/html/upload.html");
            webDriver.findElement(cssSelector("input[value='Need Confirm']")).click();
            webDriver.switchTo().alert().accept();
        }
    
        @Test
        public void clickConfirm() {
            Page page = CHROME.load("http://localhost:63342/seleniumcapsules/html/upload.html");
            page.button(NEED_CONIRM).click();
            page.accept();
        }
    }
    

    Sunday, June 8, 2014

    upload file

    Some applications may require user to upload the file and since it is part of the workflow, without it, the test can be executed end to end, so it is necessary to add the support for file upload.

    Actually file upload is simpler than you think, even it appears like you have to browser the folder to find the file, but it is not a case when you do test automation. The browsing part of the function of the browser, it is out of your control so you don't need to test that part. As tester, we only test the part we are responsible for, for example, after you choose the file, click the button and the file should be sent to server and process by the application. So we can use put the file under test source folder and use Java File to locate it and convert the file into a file and use it for the testing.





    We can put the file to be uploaded inside /test/resources/upload/ folder and refer it in the test.



    The button doesn't submit the form, it just alert a message instead, so the last part of the test is to accept the alert.

    The test can be managed by Spring as well,

    locating

    I found out one commonality between all form elements,

    1. They all have these two instance fields, Where and Locator;
    2. They all call locator.locate(where) before taking other actions.

    To use one word to describe this activity of the locator, it is "locating"

    Thus this class Locating is introduced into Selenium Capsules framework,
    public class Locating<Where extends Searchable<Where>, What> {
    
        protected final Where where;
        protected final Locator<Where, What> locator;
    
        /**
         * Constructor of the Locating.
         *
         * @param where   where
         * @param locator locator
         */
        public Locating(Where where, Locator<Where, What> locator) {
            this.where = where;
            this.locator = locator;
        }
    
        public What locate() {
            return locator.locate(where);
        }
    }
    


    And now Input is
    public class Input<Where extends Searchable<Where>> extends Locating<Where, Element> {
    
        public static final Logger log = getLogger(Input.class);
    
        /**
         * Constructor of the input field.
         *
         * @param where    where
         * @param selector selector
         */
        public Input(Where where, Supplier<By> selector) {
            super(where, Locators.<Where>tryElement(selector));
        }
    
        /**
         * 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 getValue() {
            final Retry retry = new Retry(5, 1, SECONDS);
            try {
                retry.attempt(() -> {
                    log.info("{}", retry);
                    Element element = locate();
                    return VALUE.locate(element);
                });
            } catch (Exception e) {
                log.info("Failed to read text", e);
            }
            return null;
        }
    
        /**
         * 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) {
            String string = value.toString();
            final Retry retry = new Retry(5, 1, SECONDS);
            try {
                retry.attempt(() -> {
                    log.info("{}", retry);
                    Element element = locate();
                    element.clear();
                    element.sendKeys(string);
                    if (VALUE.and(new IsStringEqual(string)).test(element)) {
                        retry.off();
                    }
                    return null;
    
                });
            } catch (Exception e) {
                log.info("Failed to set text {}", string);
            }
        }
    
        /**
         * Test the autocomplete function for the input by given value, click the element
         * on the suggestion list which matches 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) {
            Element element = locate();
            element.clear();
            Element suggestion;
            for (char c : value.toString().toCharArray()) {
                element.sendKeys(String.valueOf(c));
                suggestion = locator.locate(where);
                if (suggestion != null) {
                    suggestion.click();
                    return;
                }
            }
            suggestion = where.until(locator);
            if (suggestion != null) {
                suggestion.click();
            }
        }
    }
    
    


    Checkbox became,

    public class Checkbox<Where extends Searchable<Where>> extends Locating<Where, Element> {
    
        /**
         * Constructor of the checkbox.
         *
         * @param where    the place the checkbox can be found
         * @param selector the selector that leads to the checkbox
         */
        public Checkbox(final Where where, Supplier<By> selector) {
            super(where, element(selector));
        }
    
        /**
         * Change the checkbox according to the value parameter
         *
         * @param value true or false
         */
        public void setValue(boolean value) {
            Element checkbox = locate();
            if (checkbox != null && checkbox.isSelected() != value) {
                checkbox.click();
            }
        }
    
        /**
         * @return whether the checkbox is checked or not
         */
        public boolean isChecked() {
            return CHECKED.and(TRUE).test(locate());
        }
    }
    
    


    Radio became,

    public class RadioButton<Where extends Searchable<Where>> extends Locating<Where, Stream&tl;Element>> {
    
        /**
         * Constructor this radio button.
         *
         * @param where    where
         * @param selector selector
         */
        public RadioButton(Where where, Supplier<By> selector) {
            super(where, elements(selector));
        }
    
        /**
         * @param value value to set
         */
        public void setValue(Object value) {
            new FirstMatch<>(DISPLAYED.and(VALUE.and(new IsStringEqual(value))))
                    .and(CLICK_IF_NOT_NULL)
                    .locate(locate());
        }
    
        /**
         * @return the value of the select radio
         */
        public String getValue() {
            return new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE)))
                    .and(VALUE)
                    .locate(locate());
        }
    }
    
    


    There no longer have these two instance fields,

    protected final Where where;
    protected final Locator locator;


    And locator.locate(where) became locate(). There is change in the function call chain as well, radioButtonGroup used to be the first function in the chain and now it is FirstMatch, after radioButtonGroup became the locator variable in the super class and locator.locate(where) became locate(), to illustrate the functional transformation, the following three function calls have the same effect.
    
            radioButtonGroup
                    .and(new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))))
                    .and(VALUE)
                    .locate(where);
    
            new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE)))
                    .and(VALUE)
                    .locate(radioButtonGroup.locate(where));
    
           
            VALUE.locate(
                    new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))).locate(
                          radioButtonGroup.locate(where)));
    
    


    They all are equivalent to this sequential form,

         Stream<Element> radios = radioButtonGroup.locate(where);
         Element radio = new FirstMatch<>(DISPLAYED.and(CHECKED.and(TRUE))).locate(radios);
         String value = VALUE.locate(radio);
    


    which is exactly same to this raw form, without Selenium Capsules, you can see a lot of Selenium powder.
    
        String value = null;
        List<WebElement> radios = webDriver.findElements(By.name("customFieldDS.customfield_ROW0_value"));
        for (WebElement radio : radios) {
           if (radio.getAttribute("checked").equals("true")) {
               value = radio.getAttribute("value"));
           }
        }
    

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