So we've moved from using Selenium to Selenium 2. All in all, this has been a positive experience up to now. The new API does a very good job in overcoming the problems we had with Selenium 1. It fells quite object-oriented, and I've had my share of learning from the fluidity and the expressiveness, especially with the "lift" Finder API.
But alas, some problems have migrated along with us, as well. Foremost these surface when dealing with Ajax. While the Selenium API has some patterns to deal with that (for example it waits implicitly if a command causes a page to load), these do not work too well for our use of Ajax. It seems geared to web application that don't use too much Ajax. But that doesn't mean it can't help with an Ajax-heavy application. We'll just have to see how.
To find out, we will create a simple Web Application that uses Ajax to update a panel and see how Selenium 2 deals with this situation. We'll do this with wicket because it's pretty easy and quite fun, too.
So, first we instantiate a Maven Project using
mvn archetype:generate and using the wicket-quickstart archetype. I selected version 1.4.7, which is not quite up to date, so I pimped it up to 1.4.16 in the pom. While being there, add Dependencies for Selenium and jUnit and correct the one for the maven-jetty-plugin. Throw in a dependency on hamcrest-all for good measure, we're going to use it later. The final pom looks like this for me:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.tk.examples</groupId>
<artifactId>wicket-selenium</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>wicket-selenium-example</name>
<description></description>
<dependencies>
<!-- WICKET DEPENDENCIES -->
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket</artifactId>
<version>${wicket.version}</version>
</dependency>
<!-- LOGGING DEPENDENCIES - LOG4J -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
<!-- JUNIT DEPENDENCY FOR TESTING -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<!-- JETTY DEPENDENCIES FOR TESTING -->
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>${jetty.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${jetty.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-management</artifactId>
<version>${jetty.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium</artifactId>
<version>2.0b3</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>2.0b3</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
<resource>
<filtering>false</filtering>
<directory>src/main/java</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
<testResources>
<testResource>
<filtering>false</filtering>
<directory>src/test/java</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</testResource>
</testResources>
<plugins>
<plugin>
<inherited>true</inherited>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
<optimize>true</optimize>
<debug>true</debug>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>${jetty.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<downloadSources>true</downloadSources>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<wicket.version>1.4.16</wicket.version>
<jetty.version>6.1.4</jetty.version>
</properties>
</project>
Import this into Eclipse any way you like. I'll use the m2eclipse and run the project using
mvn jetty:run for now.
In the Wicket application, we just add a simple Panel that will update itself when clicked:
package de.tk.examples;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
public class UpdatingPanel extends Panel {
int timesClicked=0;
String messageText = "This panel has not been updated";
public UpdatingPanel(String id, IModel model) {
super(id, model);
}
public UpdatingPanel(String id) {
super(id);
}
@Override
protected void onConfigure() {
super.onConfigure();
addOrReplace(new Label("message", messageText)
.setEscapeModelStrings(false));
add(new AjaxEventBehavior("onClick") {
@Override
protected void onEvent(AjaxRequestTarget target) {
messageText = "This panel has been updated
"+(++timesClicked)+" times.";
target.addComponent(UpdatingPanel.this);
}
});
}
}
Wicket being Wicket, this goes along with the necessary markup file:
<html xmlns:wicket=
"http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
<head>
<title>Self-Updating Ajax Panel</title>
</head>
<body>
<wicket:panel>
<strong>This panel updates itself when you click it.</strong>
<br/><br/>
<span wicket:id="message">This is the default text</span>
</wicket:panel>
</body>
</html>
What do we have now? The Panel will initially say that it has not been clicked yet. Once clicked, it will replace itself with a new version of itself, saying how often it did that. This way we can easily check if every click got processed.
So, along for the testing. We'll start by using a naive approach - we just find the element and click it, three times. Then we check the message text to see if all went well:
@Test
public void findPanelWhileHoldingOnToTheElement() {
WebDriver driver = new FirefoxDriver();
driver.navigate().to("http://localhost:8080/wicket-selenium/");
WebElement element = driver.findElement(By.cssSelector("div"));
element.click();
element.click();
element.click();
}
Let's do a
mvn jetty:run and run the test - and be slightly disappointed that it fails, complaining about a StaleElementException it got. What happened? Well, after the first click the Element got replaced. So the WebElement you're holding is not on the page anymore. It was replaced by another, quite similar one, but with different text. You might think 'well, so what? It's still the same thing, just give me text!', but you wouldn't really want that. Because it is not the same thing, it's a second instance. Imagine you had a JavaScript listener it it, one that not on the second version of that element anymore. Or the text is different, as is the case with this one. You're likely to want to know about that.
Now knowing one thing more, we can try a slightly smarter Approach: we'll just re-find the element every time we access it.
@Test
public void findPanelWhileRefindingTheElement() throws Exception {
WebDriver driver = new FirefoxDriver();
driver.navigate().to("http://localhost:8080/wicket-selenium/");
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
assertThat(driver.findElement(By.cssSelector("div"))
.getText(), containsString("3 times"));
}
Ah, so now it passes! Not very surprising, but satisfying nonetheless. But this approach is brittle, too. Imagine the Ajax call takes a while, let's say one second. We can simulate this by putting a Sleep in the EventHandler:
@Override
protected void onEvent(AjaxRequestTarget target) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
messageText = "This panel has been updated "
+(++timesClicked)+" times.";
target.addComponent(UpdatingPanel.this);
}
(Please note that I am NOT advising you to program like this. This is just some quick and dirty test-driven exploration.)
Start the test again. (remember to restart Jetty ;-) What I got is the following JUnit error:
java.lang.AssertionError:
Expected: a string containing "3 times"
got: "This panel updates itself when you click it.\n\nThis panel has not been updated"
This one struck me dumb for a while. What I finally figured out is this: The Selemium commands seem to get queued and executed as quickly as possible. Which actually makes sense, if you think about it, the 'a' stands for 'asynchronous'. So, while the Ajax requests rumble along slowly, the assertion extracts the text of the panel and checks it. But up to now it has not even been replaced once, hence jUnit's complaint. If you look closely, you'll see that the test even ends as failed while the first request is still pending. While we could peruse the source code to look if this assumption is true, we might as well explore with a new test case:
@Test
public void findPanelWhileRefindingTheElement() throws Exception {
WebDriver driver = new FirefoxDriver();
driver.navigate().to("http://localhost:8080/wicket-selenium/");
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
// sleep longer than the ajax requests need
Thread.sleep(4000);
assertThat(driver.findElement(By.cssSelector("div"))
.getText(), containsString("3 times"));
}
Now it passes again.
So, what do we do with this? Guessing how long the request will take is not an option. If the Ajax request would bring some new element to the page we could check repeatedly for its presence. This was a viable option in Selenium 1 and it still is. Selenium even provides helper classes for this (more on that later, perhaps). But our request just re-renders the same elements. Or does it? A change in the text is a change in the DOM, too. So I don't see why checking for this shouldn't work. Let's try it out:
@Test
public void findPanelWhileRefindingTheElement() throws Exception {
WebDriver driver = new FirefoxDriver();
driver.navigate().to("http://localhost:8080/wicket-selenium/");
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
driver.findElement(By.cssSelector("div")).click();
String text = driver.findElement(By.cssSelector("div")).getText();
while (!text.contains("3 times")) {
Thread.sleep(500);
text = driver.findElement(By.cssSelector("div")).getText();
}
assertThat(driver.findElement(By.cssSelector("div"))
.getText(), containsString("3 times"));
}
The result is rather promising, if clumsily achieved. The test passes, and it does not end before the Ajax requests do. We can actually do better, since we're using wicket.
We can take advantage of Wicket's Ajax implementation, which allows for adding of Listeners before and after each Ajax request. Let's try if we can use these to observe our calls and wait for them to finish.
@Test
public void canObserveWicketAjaxViaJavaScript() throws Exception {
final WebDriver driver = new FirefoxDriver();
driver.navigate().to("http://localhost:8080/wicket-selenium/");
// register wicket handlers
final JavascriptExecutor js = ((JavascriptExecutor) driver);
js.executeScript(
"if (typeof tk == 'undefined') tk = "
+ "{activeAjaxCount: 0, ajaxCallsTried: 0, ajaxCallsCompleted: 0}; "
+ "Wicket.Ajax.registerPreCallHandler(function()"
+"{tk.activeAjaxCount++;tk.ajaxCallsTried++;});"
+ "Wicket.Ajax.registerPostCallHandler(function()"
+"{tk.activeAjaxCount--;tk.ajaxCallsCompleted++;});");
// start ajax requests
new DivFinder().findFrom(driver).iterator().next().click();
new DivFinder().findFrom(driver).iterator().next().click();
new DivFinder().findFrom(driver).iterator().next().click();
// and wait for processing
new Wait("Ajax calls did not finish in time!") {
public boolean until() {
return (Boolean) js
.executeScript("return tk.activeAjaxCount == 0 "
+"&& tk.ajaxCallsCompleted==3");
}
}.wait("waiting for finishing Ajax Call");
// Finder API for expressive condition checks
WebElement div = new DivFinder().findFrom(driver).iterator().next();
assertThat(div.getText(), containsString("3 times"));
}
Run the tests, and rejoice! It works.
Let's summarize a bit. Selenium 2 does a great job on static pages. It also does quite a good job on pages that use Ajax. What can happen to you when you use WebElements as a basis, however, is that they grow stale on you when an Ajax request is sent. This can actually happen in a rather unpleasant way - in one line of code, the WebElement behaves just fine, in the next it's gone. Since it is rather unproductive to wrap each access in a try/catch and refresh the WebElement when necessary, I'd advise to have a look at the Finders in the Selenium Lift API. We've just used one of those in the last test. They take some getting used to, if you're not so much into predicates and the like, but I for one really like the expressiveness they give the code. On the plus side, you can use that style in jUnit to receive better error messages with assertThat(...) than with the simpler assertion messages. And it's a great example of the flexibility you gain when using objects even for simple tasks, like filtering a list of elements on some predicate. This lesson alone is worth a lot.
While the wait-for-ajax part in the last test can certainly benefit from some heavy refactoring and will not work in every situation, it shows how you can deal with the need to wait for Ajax in Wicket applications. Using a test-style approach you can easily experiment with different scenarios. When you find something you want to use, refactor the code out, or even better, TDD yourself a proper implementation based on your experiments. That way, you still have you old tests/experiments to retrace your steps.