Tuesday, August 19, 2014

Detecting Ajax completion in JQuery

One of the annoying problems of waiting for an ajax action to complete in a typical Selenium test is you end up polling for that expected element to change, and wait there much longer than you have to for your test to fail.

This can be annoying when running in CI, and you have 100's to 1000's of test.  If one service is down, then every test that exercises that service can end up waiting it's maximum wait time, causing your results to come in much slower than it should.

Polling for elements can also waste a lot of CPU and memory on the host system on pages that have high amounts of nested elements.  This is because each element find operation requires javascript code to traverse the DOM to find the target element.

Ideally if your ajax request is throwing a 500 error and nothing is happening on the screen, you want to fail this test as soon as that could be detected.  However, selenium does not expose the network layer of your browser, so you don't know what's going on under the hood.

Fortunately some frameworks have callbacks

JQuery for example has a '.ajaxStop()' call back that can be used to register handlers when all outgoing ajax operations are completed.

 //C#
class JQueryUtil
class JQueryUtil
{
public static void InjectAjaxLatch(IWebDriver driver)
{
driver.ExecuteJavaScript<bool>("window._FTW_js_latch = false; return true;");
driver.ExecuteJavaScript<bool>("$(document).ajaxStop(function(){window._FTW_js_latch = true;});return true;");
}
public static void WaitJsLatch(IWebDriver driver, TimeSpan waitTime=default(TimeSpan))
{
const string script = "return window._FTW_js_latch;";
FtwExecutionUtils.WaitUntil(()=>driver.ExecuteJavaScript<bool>(script) == true, waitTime);
}
public static void WaitUntil(Func<bool> condition,
TimeSpan timeout = default(TimeSpan),
TimeSpan pollingInterval = default(TimeSpan))
{
var sw = new Stopwatch();
sw.Start();
while (sw.Elapsed < timeout)
{
if (condition)
{
return;
}
Thread.Sleep(pollingInterval);
}
throw new TimeoutException(".WaitUntil() call has timed out.");
}
}


How this works is pretty simple.  First execute the top method, it'll set a variable to create a call back to set a JavaScript latch that tells us when all ajax operations complete.  Then you perform your action, like enter that search query on some live search form.  Then immediately after, we call the WaitJSLatch method which creates a simple wait condition that polls on the JavasScript latch waiting for all ajax operations to complete.

The benefits of this is allowing our tests to fail faster, and also run a bit leaner.  Since locating elements in Selenium is a pretty expensive operation on a page with high levels of nested elements.  JavaScript injection will complete faster and causing less stress on the CPU of the host system.  This is especially useful when running on Selenium grid on a node with many browsers running at the same time.

No comments: