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

    No comments:

    Post a Comment