Testing GWT Apps with Selenium or WebDriver
Both Selenium and WebDriver (which is essentially now the successor to Selenium) provide a good way to functionally test web applications in multiple target environments without manual work. In the past, web UIs were built using the page navigation to allow users to submit forms, etc. These days, more and more web applications use Ajax and therefore act and look a lot more like desktop applications. However, this poses problems for testing – Selenium and WebDriver are designed to work with user interations resulting in page navigation and don’t play well with AJAX apps out of the box.
GWT-based applications in particular have this problem, but there are some ways I’ve found to develop useful and effective tests. GWT also poses other issues in regards to simulating user input and locating DOM elements, and I discuss those below. Note that my code examples use Groovy to make them concise, but they can be pretty easily converted to Java code.
Problem 1: Handling Asynchronous Changes
One issue that developers face pretty quickly when testing applications based on GWT is detecting and waiting for a response to user interaction. For example, a user may click a button which results in an AJAX call which would either succeed and close a window or, alternatively, show an error message. What we need is a way to block until we see the expected changes, with a timeout so we can fail if we don’t see the expected changes.
Solution: Use WebDriverWait
The easiest way to do this is by taking advantage of the WebDriverWait (or Selenium’s Wait). This allows you to wait on a condition and proceed when it evaluates to true. Below I use Groovy code for the conciseness of using closures, but the same can be done in Java, though with a bit more code due to the need for anonymous classes.
def waitForCondition(Closure closure) { int timeout = 20 WebDriverWait w = new WebDriverWait(driver, timeout) w.until({ closure() // wait until this closure evaluates to true } as ExpectedCondition) } def waitForElement(By finder) { waitForCondition { driver.findElements(finder).size() > 0; } } def waitForElementRemoval(By finder) { waitForCondition { driver.findElements(finder).size() == 0; } } // now some sample test code submitButton.click() // submit a form // wait for the expected error summary to show up waitForElement(By.xpath("//div[@class='error-summary']")) // maybe some more verification here to check the expected errors // ... correct error and resubmit submitButton.click() waitForElementRemoval(By.xpath("//div[@class='error-summary']")) waitForElementRemoval(By.id("windowId"))
As you can see from the example, your code can focus on the actual test logic while handling the asynchronous nature of GWT applications seamlessly.
Problem 2: Locating Elements when you have little control over DOM
In web applications that use templating (JSPs, Velocity, JSF, etc.), you have good control and easy visibility into the DOM structure that your pages will have. With GWT, this isn’t always the case. Often, you’re dealing with nested elements that you can’t control at a fine level.
With WebDriver and Selenium, you can target elements using a few methods, but the most useful are by DOM element ID and XPath. How can we leverage these to get maintainable tests that don’t break with minor layout changes?
Solution: Use XPath combined with IDs to limit scope
In my experience, to develop functional GWT tests in WebDriver, you should use somewhat loose XPath as your primary means of locating elements, and supplement it by scoping these calls by DOM ID, where applicable.
In particular, use IDs at top level elements like windows or tabs that are unique in your application and won’t exist more than once in a page. These can help scope your XPath expressions, which can look for window or form titles, field labels, etc.
Here are some examples to get you going. Note that we use // and * in our XPath to keep our expressions flexible so that layout changes do not break our tests unless they are major.
By byUserName = By.xpath("//*[@id='userTab']//*[text()='User Name']/..//input") WebElement userNameField = webDriver.findElement(byUserName) userNameField.sendKeys("my new user") // maybe a user click and then wait for the window to disappear By submitLocator = By.xpath("//*[@id='userTab']//input[@type='submit']") WebElement submit = webDriver.findElement(submitLocator) submit.click() // use our helper method from Problem 1 waitForElementRemoval By.id("userTab")
Problem 3: Normal element interaction methods don’t work!
GWT and derivatives (Vaadin, GXT, etc.) often are doing some magic behind the scenes as far as managing the state of the DOM goes. To the developer, this means you’re not always dealing with plain <input> or <select>, etc. elements. Simply setting the value of the field through normal means may not work, and using WebDriver or Selenium’s click methods may not work.
WebDriver has improved in this regard, but issues still persist.
Solution: Unfortunately, just some workarounds
The main problems you’re likely to encounter relate to typing into fields and clicking elements.
Here are some variants that I have found necessary in the past to get around clicks not working as expected. Try them if you are hitting issues. The examples are in Selenium, but they can be adapted to the corresponding calls in WebDriver if you require them. You may also use the Selenium adapter for WebDriver (WebDriverBackedSelenium) if you want to use the examples directly.
CLICK ISSUES
Sometimes elements won’t respond to a click() call in Selenium or WebDriver. In these cases, you usually have to simulate events in the browser. This was true more of Selenium before 2.0 than WebDriver.
// Selenium's click sometimes has to be simulated with events. def fullMouseClick(String locator) { selenium.mouseOver locator selenium.mouseDown locator selenium.mouseUp locator } // In some cases you need only mouseDown, as mouseUp may be // handled the same as mouseDown. // For example, this could result in a table row being selected, then deselected. def mouseOverAndDown(String locator) { selenium.mouseOver locator selenium.mouseDown locator }
TYPING ISSUES
These are the roundabout methods of typing I have been able to use successfully in the past when GWT doesn’t recognize typed input.
// fires only key events (works for most GWT inputs) // Useful if WebDriver sendKeys() or Selenium type() aren't cooperating. def typeWithEvents(String locator, String text) { def keyEvents = ["keydown", "keypress", "keyup"] typeWithEvents(locator, text, keyEvents) } // fires key events, plus blur and focus for really picky cases def typeWithFullEvents(String locator, String text) { def fullEvents = ["keydown", "keypress", "keyup", "blur", "focus"] typeWithEvents(locator, text, fullEvents) } // use this directly to customize which events are fired def typeWithEvents(String locator, String text, def events) { text.eachWithIndex { ch, i -> selenium.type locator, text.substring(0, i+1) events.each{ event -> selenium.fireEvent locator, event } } }
Note that the exact method that works will have to be figured out by trial-and-error and in some cases, you may get different behaviour in different browsers, so if you run your functional tests against different environments, you’ll have to ensure your method works for all of them.
Conclusion
Hopefully some of you find these tips useful. There are similar tips out there but I wanted to compile a good set of examples and workarounds so that others in similar situations don’t hit dead-ends or waste time on problems that require lots of guessing and time.
Reference: Testing GWT Apps with Selenium or WebDriver from our JCG partners at the Carfey Software blog.
It seems like most of your problems are from using 3rd party UI libraries like Vaadin/Sencha/etc. FWIW, I use the stock GWT widgets (TextBox/ListBox) and don’t have any of these typing/clicking/etc. problems. Nor problems with not controlling IDs in the DOM.
Depending on how your GWT developers (or the rich UI framework you’re using) do their AJAX calls, you can also solve the wait problem (almost) entirely, see this post I did a few months ago:
http://draconianoverlord.com/2011/10/14/sane-selenium-testing.html
With that, the only place in my tests where I need explicit waits is for animation.