Tuesday, August 26, 2008

Ajax and Selenium: waitForCondition()

Selenium is a very useful tool but it can be very, very obtuse.

One challenge is dealing with Ajax; you might click on a button, but without a full page refresh, it's hard to know when to look for expected changes via Ajax and DHTML.

In the past, my test suites had short sleeps, a few hundred milliseconds. This makes them fail sporadically ... every once and a while on my MacBook Pro I'm doing so much other stuff while the tests run that the timing goes screwy.

You're then left with a difficult choice: sleep too short and the tests may fail. Sleep too long and your tests will always be slow.

Fortunately, there's a third option: Selenium's waitForCondition call. Of course, their documentation is worthless.

What it is supposed to do is evaluate a JavaScript snippet repeatedly, until the snippet returns true. However, it's tricky to get right. Like much in JavaScript, it's about context.

In my case, I wanted to wait for a client-side popup <div> to appear:

        type("amount", "abc");
        type("quantity", "abc");

        click(SUBMIT);

        waitForCondition("document.getElementById('amount:errorpopup')", "5000");

        assertText("//div[@id='amount:errorpopup']/span", "You must provide a numeric value for Amount.");
        assertText("//div[@id='quantity:errorpopup']/span", "Provide quantity as a number.");

JavaScript treats null as false, and getElementById() returns null if an element with the id does not exist.

I'm making the assumption that once one of two <div> elements appears, they both will. I then use some XPath to get the text inside the <span> inside each <div>, to make sure the correct message was displayed to the user.

But this code doesn't work.

The problem is that document isn't what you'd expect; I'm guessing that it's some other frame inside the browser (Selenium's UI and code executes in one frame, which runs the actual application inside the second frame).

The solution took some research and the sacrifice of a few small furry animals to obtain:

        waitForCondition("selenium.browserbot.getCurrentWindow().document.getElementById('amount:errorpopup')", "5000");

That works, and it works much faster than adding a Thread.sleep() in the middle of my code.

8 comments:

  1. Ran in to a similar problem/solution wrt ajax and selenium testing.

    We also wrapped the base selenium test object so that we could do streamlined chained calls like:

    click(SUBMIT).waitForItemAppear("unique id of element");

    that's pseudo code but more or less how it worked

    ReplyDelete
  2. I had same problem, but I solved it by cycle in the Java code which periodically checks for condition with small sleep.

    ReplyDelete
  3. Heya,

    Selenium RC is a close relative of Core, and in Core reference we read:

    Note that, by default, the snippet will run in the context of the "selenium" object itself, so

    this

    will refer to the Selenium object. Use

    window

    to refer to the window of your application, e.g.

    window.document.getElementById('foo')

    If you need to use a locator to refer to a single element in your application page, you can use

    this.browserbot.findElement("id=foo")

    where "id=foo" is your locator.


    Thats why your code did not work properly - you should use "window.document" instead of just "document". And you can even use (if I am not mistaken) "this.browserbot.findElement()" to use XPath etc.

    ReplyDelete
  4. Since it's trivial to add custom commands to selenium, i'd suggest authoring a waitForAjax command.

    It would just be used in place of the 'short sleeps'

    ReplyDelete
  5. Hey, I was having similar problems with AJAX, and this solution worked great for me.

    Thanks for the help!

    ReplyDelete
  6. nice work! that worked for my ajax site.

    I had to use window.document.getElementById

    ReplyDelete
  7. Anonymous9:51 PM

    My java code is selenium.waitForCondition("function getdivHTML() {" +
    " var divArray = selenium.browserbot.getCurrentWindow().document.getElementsByTagName('textarea');" +
    " for (var i = 0; i#14) in at line number 14

    Suggestions please.

    ReplyDelete

Please note that this is not a support forum for Tapestry. Requests for help will be deleted. Please subscribe to the Tapestry user mailing list if you are in need of support, or contact me directly for professional (for pay) support.

Spammers: Don't bother. I delete your comments and it's a waste of time for both of us. 垃圾邮件发送者:不要打扰。我删除您的评论和它的时间对我们双方的浪费