Selenium and Ajax Requests

Posted by Lance Ivy Mon, 05 Nov 2007 18:26:00 GMT

It happens to all programmers sooner or later: you sink a day into trying to save a couple seconds, and afterwards your only hope of making it all worthwhile is to share your findings.

I desperately needed to test my Rails AJAX behaviors, so yesterday I dug in and learned Selenium. For Rails integration I decided to work with SeleniumOnRails.

Bit of a learning curve there. Eventually I figured out how to speak Selenese, how to patch selenium-on-rails to clear the fixture cache, and how to rebuild my Solr index after reloading fixtures. Things were finally going well. But I had one problem left: asynchronous AJAX.

Selenium Meets AJAX

Selenium does great waiting for pages to load before proceeding, since it’s all been baked into the framework. But if you want to wait for XHR requests (and if you didn’t, should you be using Selenium?), you have two options:

  1. Pause after each XHR request, as recommended by this IBM DeveloperWorks article
  2. Use waitForCondition to check for side-effects, as recommended by Joseph Moore

Both of these options are, shall we say, dissatisfying. With the first option you’ve got to try and set a pause interval that a) is long enough to guarantee the test runs no matter how long you’ve had Firefox open and b) doesn’t grate on your nerves everytime your tests take seconds longer to run than they should. And with the second option you’ve got to wait for side-effects that you should be verifying instead.

Surely there’s a better way!

A Better Way

Phew. That was a close one.

Like many of you reading this, I use Prototype. The key to the solution I developed depends on registering with the lightweight Ajax.Responders event system.

I tried many approaches. I failed a lot. But you’ve been reading long enough, and you just want something that works, yes? Wait no longer. Add this to your user-extensions.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * Registers with the Prototype library to record when an Ajax request finishes.
 * Call this after the most recent page load but before any Ajax requests.
 *
 * Once you've called this for a page, you should call waitForAjaxRequest at
 * every opportunity, to make sure the AjaxRequestFinished flag is consumed.
 */
Selenium.prototype.doWatchAjaxRequests = function() {
  selenium.browserbot.getCurrentWindow().Ajax.Responders.register({
    onComplete: function() {Selenium.AjaxRequestFinished = true}
  });
}
/**
 * If you've set up with watchAjaxRequests, this routine will wait until
 * an Ajax request has finished and then return.
 */
Selenium.prototype.doWaitForAjaxRequest = function(timeout) {
  return Selenium.decorateFunctionWithTimeout(function() {
    if (Selenium.AjaxRequestFinished) {
      Selenium.AjaxRequestFinished = false;
      return true;
    }
    return false;
  }, timeout);
}
Selenium.AjaxRequestFinished = false;

And you can now write test tables like:

open /page/with/ajax/links
watchAjaxRequests
click my-ajax-link
waitForAjaxRequest 1000

Ahhhh, relief. So much better than pause hacks or waiting for something to change on the page.


Comments

  1. Avatar
    Tim Harper 15 days later:

    This suggestion worked awesome, Lance!

    For the record (I’m new to selenium), I put the user-extensions.js file in vendor/plugins/selenium-on-rails/selenium-core/scripts/user-extensions.js

    Also, I had to go into the Selenium IDE and add the user-extensions.js path to it, in order for the Selinum IDE runner to work.

    Additionally, for rselenese files, I had to add the following methods to SeleniumOnRails::TestBuilderActions

    def watch_ajax_requests
      command "watchAjaxRequests" 
    end
    
    def wait_for_ajax_request(seconds)
      command "waitForAjaxRequest", seconds
    end
    

    You’re awesome Lance! Thanks for sharing!

    Tim

  2. Avatar
    Tim Harper 15 days later:

    Oh… I was going to say.

    Even though I waited for the Ajax call to complete, I still needed a bit of additional wait time for the response html to be inserted into the document. If I find an elegant solution for that, I’ll share.

  3. Avatar
    Lance Ivy 15 days later:

    Thanks for filling in those details, Tim!

    I haven’t yet had a problem with waiting for the AJAX request to actually be processed, though it sounds like the sort of situation where you’d have no choice but to fall back to pausing or testing for side effects.

    Hmm, maybe you could modify the doWatchAjaxRequests function to use setTimeout() for updating the Selenium.AjaxRequestFinished property? I’ve found that with JavaScript sometimes all you need to do is give it the tiniest of timeouts (50ms) to fix timing issues.

  4. Avatar
    Tim Harper about 1 month later:

    Oddly enough, I couldn’t get any timeout to be reliable. It’s really unfortunate!

    To compensate, I came up with the following rselenese helpers:

    module SeleniumOnRails
      module TestBuilderActions
    
        %w[click type select verify_value assert_selected_label].each{|method_name|
          class_eval <<-EOF
            def wait_for_and_#{method_name}(*args)
              wait_for_element_present(args.first)
              #{method_name}(*args)
            end
    
            def #{method_name}_and_wait_for_not_present(*args)
              #{method_name}(*args)
              wait_for_element_not_present(args.first)
            end
          EOF
        }
      end
    end
    

    This allows you to call things like:

    wait_for_and_click "link" 
    click_and_wait_for_not_present "link" 
    wait_for_and_type "record_name", "Tim" 
    

    etc…

    Not as cool as your Ajax wait solution, but it’s proving to more dependable

  5. Avatar
    Lance about 1 month later:

    Hmm, your method is basically a variation of waiting for a side effect to manifest. It makes sense that’s what you’d need to solve your problem.

    I’m still not sure why you have your problem, though. According to the prototype docs, onComplete is called after any automatic behaviors, so it seems your problem would only arise if other important things happened in later onComplete callbacks.

  6. Avatar
    Gary about 1 month later:

    Hi Lance,

    I’m a newbie to a lot of these methods here but I managed to DL selenium IDE and I have this and TestRunner.hta working for me from my desktop.

    I copy and pasted the above code into my ” user-extensions.js ” file. I can see the new dropdowns from Selenium IDE for Firefox.. watchforajax and waitforajax.. I’m not sure how they are working ..

    1. Do I need to manually insert new commands between click commands? Or should they work automatically detecting ajax functionality somehow?

    2. How much time should I place for the value? ” waitForAjaxRequest 1000 ” ?

    3. Here’s some errors I’m getting:

    1. [info] Executing: |clickAndWait | link=My Team | |
    2. [warn] triggerMouseEvent assumes setting screenX and screenY to 0 is ok
    3. [info] element doesn’t have fireEvent
    4. [info] element has initMouseEvent
    5. [info] Executing: |assertTitle | My Team | |
    6. [info] Executing: |click | ctl00_ContentPlaceHolder1_rptSubs_ctl00_laPosition | |
    7. [warn] triggerMouseEvent assumes setting screenX and screenY to 0 is ok
    8. [info] element doesn’t have fireEvent
    9. [info] element has initMouseEvent
    10. [info] Executing: |click | ctl00_ContentPlaceHolder1_ucRightPanel_wsTask_spIconText | |
    11. [warn] triggerMouseEvent assumes setting screenX and screenY to 0 is ok
    12. [info] element doesn’t have fireEvent
    13. [info] element has initMouseEvent
    14. [info] Executing: |waitForAjaxRequest | | 500 |
    15. [error] Timeout is not a number: ’’
    16. [info] Executing: |waitForAjaxRequest | | 500 |
    17. [error] Timeout is not a number: ’’

    4. This is a copy of the Source HTML code:

    click ctl00_ContentPlaceHolder1_ucRightPanel_wsTask_spIconText waitForAjaxRequest 500 click ctl00_ContentPlaceHolder1_ucRightPanel_wsProfile_spIconText waitForAjaxRequest 1000

    I have tried to use the pause command but this works temporarly and is not always consistant.. waitforcondition is also giving me problems.. Please help if you can.

    thx,

    G

  7. Avatar
    Jason 3 months later:

    Lance,

    I’m really in the same boat as Gary…Do you have any tips for getting this set up? The functions are available in the command list, but they don’t seem to work. Perhaps I am not using the right “values” or it is something much deeper. Any help would be much appreciated…

    Thanks,

    Jason

  8. Avatar
    Lance 3 months later:

    Gary and Jason, if the error messages in step #3 are any indication, the timeout argument for the waitForAjaxRequest command is somehow being slotted into the wrong position. That is, it should be:

    [info] Executing: |waitForAjaxRequest | 500 | |

    and not:

    [info] Executing: |waitForAjaxRequest | | 500 |

    That’s where I’d start investigating, at least. I haven’t tried the Selenium IDE for Firefox.

    p.s. the timeout argument is in milliseconds. “waitForAjaxRequest | 1000 |” means “wait for an ajax request, but wait no longer than one second”.

  9. Avatar
    Jason 4 months later:

    Great extension! However, my testing suggests that it doesn’t work properly when the test is run in Selenium IDE. I added the modified user-extension into the Selenium IDE options as described by Tim, above, and I don’t receive any errors when running. However, I can see that the waitForAjaxRequest call seems to just return immediately, when the XHR call is still in progress.

    However, everything runs fine when run using the TestRunner.

  10. Avatar
    marek.bytnar@wp.pl 5 months later:

    I have problem with using selenium rc. The steps are more or less like this : First selenium creates new item in database. The views during the rest of test shows that it is present in db but when I make Model.find(:all) in the test code it returns results without this recently created.

    what is the reason ? Is there any caching mechanism in there?

  11. Avatar
    Lance 5 months later:

    Hi Marek, you may try asking on the Rails or Selenium mailing lists.

  12. Avatar
    guy_argo@yahoo.com 8 months later:

    newbie question: How do I use this trick if I’m testing via the Java Selenium API?

  13. Avatar
    Lance 8 months later:

    I’m afraid that I haven’t used the Java Selenium API. Sorry!

  14. Avatar
    Lance 8 months later:

    I talked with Guy a bit more over email, and he discovered the problem – user-extensions.js is somewhat hidden inside the selenium-server jar file. So if you’re hoping to use this trick in Java, that’s where to add the custom JavaScript.