@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,
/** * 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())); }