Matthew O'Riordan

This is where I record my rants, comment, quotes and thoughts on things. I welcome your input, please fire away. Find out more about me at: http://mattheworiordan.com

Testing :focus with JQuery and Selenium or Capybara-Webkit

** Update: If you’re using JQuery 1.8, unfortunately the fix in this post no longer works. See http://blog.mattheworiordan.com/post/33422200325/jquery-focus-pseudo-selector-fails-with-selenium-and instead.

Recently whilst trying to write integration tests for a JQuery heavy front end application, I came across a very strange issue when testing to see if an element has focus.

What I found is that if an element has focus, yet the actual browser or tab does not have focus, then using the pseudo selector :focus from JQuery (or Sizzle) will not find the focussed item.  Putting it another way, the browser and web page must have the focus in the OS for the :focus selector to work.  

A simple way to replicate this issue is to go to http://jsbin.com/abozas, open up your Javascript console, and type the following commands

$('#inpt').focus();
$('#inpt').is(':focus');

As you have just put the focus on #inpt, you would expect it to retain focus and thus pass a JQuery test of .is(‘:focus’);  However, because the Javascript console has the focus, and not the web page, .is(‘:focus’) will always return false.

Now generally this is not an issue as your web page actually has focus, but unfortunately I discovered that in Rails when using Cucumber, Capaybara and Capybara-Webkit and/or Selenium and testing for :focus, all tests will fail because the browser window does not have the focus of the OS at the time it is run.

So firstly I logged this is an issue with Capybara-Webkit, and in the mean time I’ve worked up a workaround solution by poking around in JQuery and Sizzle (the CSS selector engine that powers not only JQuery, but also PrototypeDojoMochiKitTinyMCE).  I discovered that the issue is in fact not with the Sizzle selector logic, but resides with some optimizations that Sizzle uses for performance reasons.  For the function matchesSelection, it tries to first use the native method webKitMatchesSelector() to match a selector, and if this fails, it falls back to the Sizzle selector logic.  The problem is that the webKitMatchesSelector is OS aware and thus removes focus from an element when the browser does not have focus, whereas Sizzle is unaware of the browser’s focus status and thus matches successfully using the following code:

function( elem ) {
  return elem === elem.ownerDocument.activeElement;
}

So to fix this problem, I have written a small Javascript file which you should include when you are running a test or cucumber Rails environment.  This script will then deactivate the native matches selector and query selector for your browser.

The code to fix the :focus issue can be found at https://gist.github.com/1166821 and simply does the following:

/* Prevent use of native find selector */
document.querySelectorAll = false;

/* Prevent use of native matches selector */
document.documentElement.matchesSelector = false;
document.documentElement.mozMatchesSelector = false;
document.documentElement.webkitMatchesSelector = false;
document.documentElement.msMatchesSelector = false;

If you would like to replicate this issue, review this Gist.  An example set of tests where the issue is resolved can be seen in this Gist.

I hope others find this useful.

** Update: If you’re using JQuery 1.8, unfortunately the fix in this post no longer works. See http://blog.mattheworiordan.com/post/33422200325/jquery-focus-pseudo-selector-fails-with-selenium-and instead.