Waiting for a page to load in Selenium::Remote::Driver with Selenium::Waiter
One of the first problems we come up against when trying to use Webdriver to automate a web application is handling the asynchronicity of loading a web page. For the test to be useful at all, it needs to be as fast and reliable as possible, or else there's a very real risk that devs and QA engineers will give up running the tests. The problem is that making the test run faster can sacrifice reliability if the async javascript isn't properly handled.
Instead of trying to figure out when a page loads, we should take a
hint from what an actual user does. Users don't care when a page is
done loading - they just wait for the exact element they want to
interact with, and then start clicking/inputting right away. We can
mimic that behavior with wait_until, a utility function exported by
Selenium::Waiter.
The general case of determining when a page is done loading is a variation of the halting problem. When you ask Webdriver to load a page, it does block briefly while it runs through its own algorithms to figure out when the page is done loading. But, it being the halting problem and all, they obviously can't solve it for all the cases.
my $d = Selenium::Firefox->new;
$d->get($tricky_slow_loading_page);
# this will throw when the element isn't present
my $text = $d->find_element_by_css('div')->get_text
The problem with putting in a sleep before attempting to find the
element is that it will usually work, but maybe one time in twenty it
will fail, and it won't be immediately apparent why it failed,
especially months down the line. What we want is a method that
reliably and consistently waits exactly until the element in question
is ready. We don't want to wait any longer than necessary, so a long
explicit sleep is out, but we don't want to go early, or else we'll
get exceptions all over the place. wait_until lets us get pretty
close to this ideal behavior:
# wait_until will also catch dies and croaks
my $elem = wait_until { $d->find_element_by_css('div') };
if ($elem) {
say 'Text: ' . $elem->get_text;
}
else {
say 'We waited thirty seconds without finding css=div';
}
wait_until takes a block and an optional hashref of arguments. It
wraps the block execution in a try/catch from Try::Tiny. By
default, it will run for thirty seconds, sleeping one second
between iterations. If at any point the block returns something true,
it immediately returns that value as its result. Note that
wait_until expects a block that is generally NON-blocking, so if
webdriver has an associated timeout, like the implicit wait timeout
for finding elements, you'll want to set it to a second or less if
you've increased it. The exact number of iterations will depend on how
long the block takes to execute.
To be clear, if your implicit_wait_time is 31 seconds, and you put a
find_element inside a wait_until, we'll run it once, Webdriver
itself will block for 31 seconds, and by the time we get control back
in our wait_until block, the timeout will have expired, and we'll
return control to your script after executing exactly one
find_element. (This may be the behavior you desire - but just make
sure you're doing it on purpose!)
my $d = Selenium::Firefox->new;
$d->set_implicit_wait_timeout(30000);
my $one_iteration = wait_until { $d->find_element('this is blocking', 'css') };
You can also use wait_until to have your test block until the
element is visible, or some other boolean property:
my $visible_elem = wait_until {
$d->find_element_by_id('eventually-visible')->is_displayed
};
Finally, as mentioned, wait_until wraps everything in a try, so if
the BLOCK you passed in does die, it'll get demoted to a warn. This
means you MUST check the return value of wait_until. Normally,
Selenium::Remote::Driver will croak when we run into something we
don't understand [1]. Since we're only warning, it's possible you may
get into some weird territory if you make assumptions about the return
value. Honestly, I'm still not sure about this behavior - perhaps it
makes sense for wait_until to die if the expected value never
returns true.
Although it can be frustrating, it's helpful for the program to die as close as possible to the source of the crash. Also, from the test's point of view, we have no idea what to do if we get an unexpected exception. ↩︎