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. ↩︎
We've just released version 0.25 of Selenium::Remote::Driver to the various CPANs. The big push this time around was to get around our hard dependency on the JRE. Previously, the Perl bindings demanded a standalone selenium server be operating on the browser's machine. So, if you wanted to run tests on your own box, you'd need the Java Runtime Environment installed, as the selenium-standalone-server is a .jar and needs the JRE to execute. However, as akafred pointed out, this is a prohibitive constraint (perhaps especially so for Perl programmers?).
When I first started out working with Webdriver years ago, I only used the standalone server and for a while I thought there was no other way to run the tests. Eventually, when various webdrivers began replacing Selenium RC, I found out that it was possible to "talk" directly to them with the same exact API as the standalone server, but I never connected that with the ability to avoid needing the standalone server and the JRE!
Anyway, thanks to some long-awaited prodding, Perl can now run Firefox, Chrome, and PhantomJS without the JRE! What follows are some implementation details, and simple usage examples. :)
usage
Hopefully the usage is pretty straightforward - instead of constructing a Selenium::Remote::Driver instance, use the constructor for the browser of your choice instead. Just like the Selenium standalone server, Firefox will work out of the box, as long as you have Firefox installed locally on your machine in the default location:
my $firefox = Selenium::Firefox->new; $firefox->get('http://www.mozilla.org'); # "We’re building a better Internet — Mozilla" print $firefox->get_title;
The usage is exactly analogous for the ::Chrome and ::PhantomJS classes, except you need to have the browser installed along with its associated webdriver. For PhantomJS, GhostDriver is automatically bundled for all recent versions, but for Chrome, you'll need to download the Chromedriver separately.
If your driver executables are not in your $PATH, or you'd like to specify a certain one, the constructors take a binary option that lets you tell us what executable to start. You can also specify a binary_port if there's a specific port that you'd like the webdriver server to bind to.
As expected, usage is analogous for the other two classes.
As a last note, we skipped creation of Selenium::InternetExplorer hoping that YAGNI, but if that's the browser that floats your boat, we can definitely throw a class together for the next release. Let us know in our Google group, or in the Github issues!
implementation
The implementation is ideally pretty straightforward:
Find the binary webdriver
Figure out what arguments to pass it to make it mimic a standalone server
Start it on the right port
Afterwards, clean it up so we don't orphan processes
The first step is easy - just check the $PATH/%PATH% - exceeept for Firefox, where it's apparenty quite complicated. Different operating systems install it in multiple distinctly different places, and if different versions of Firefox are named differently - basically it's seems like a big headache, and I mostly threw my hands up if the Firefox binary wasn't in the first specific default location. There's also a bit of extra complication involved to let the user choose their own path, and validating it for them afterwards.
Passing the arguments is more or less straightforward as well - exceeept for Firefox, which doesn't take arguments, as the webdriver for Firefox is actually just a Firefox extension. Luckily, we previously implemented a Firefox::Profile class, so we just use the newly renamed Selenium::Firefox::Profile to create a profile with the extension loaded. We actually now bundle the webdriver.xpi extension from the 2.45.0 version of Selenium in our release, and each time a new Selenium is released, we'll have to do a mirror release of our bindings with the updated extension. The other webdriver language bindings also start up Firefox with a few pre-compiled .so files for solving focus errors that I was also too lazy to do.
Finding an open port is simple enough, and we got to re-use Selenium::Waiter's wait_until in a few places. And, starting the webdriver up is usually easy, although at first I was a bit unfamiliar using system to start up an asynchronous process across different platforms (namely Windows...).
Finally, cleaning up the process we started is straightforward on OS X and Linux, as I think it just cleans itself up when the Perl script ends. At least, that's what some superficial tests seemed to prove - I didn't see anything in ps aux after the test was over, and I couldn't open sockets to the server port afterwards. But, on Windows, the task stays open and we need to kill it by string-matching the title of the process. It ended up being a bit of a mess!
We went through a bunch of attempts & refactors at organizing the functionality and ended up at something that's decently organized - the Selenium::CanStartBinary role implements most of the common work between the different classes. Since Firefox is special, it gets a few extra classes of its own. Meanwhile, each class holds its own arguments, default binary name, and default binary port.
conclusion
For a first pass, I think the functionality works pretty decently - as usual, I've been using it locally for a month or two now on OS X with no major issues, but I'm sure I missed a few bugs. If you run across any bugs, definitely let us know or even fix them for us, since I bet you can do it better than me! :D Cheers...
Perl Webdriver tutorials with Selenium::Remote::Driver
I've been mulling over doing a series of tutorials for using Perl and Webdriver together along with Selenium::Remote::Driver. So, I'm making this the index page of the tutorials!
Starting up: using webdrivers directly, [using the selenium-standalone-server.jar]
wait_until to handle your asynchronicity
[Finding elements without croaking]
Using Browsermob Proxy: test analytics, status codes, etc!
Drag and drop, ported over as pioneered by Dave Haeffner
Using custom Firefox profiles
Instructing the standalone server to use PhantomJS
Sometimes I write about changes in the new versions of the package:
v0.22 changes
[v0.25 changes]
There are also a number of articles about using Appium, which aims to be as no-surprises as can be when extending S:R:D.
Setting up Appium with the Perl bindings
Using Appium with a new Ionic app
Explicit step-by-step for installing Appium on Yosemite
Feel free to request articles or pitch ideas/articles if you want something not covered here!