2009-04-05 @ 20:15
Testing JavaScript Outside the Browser
The other day at LA RubyConf during the Johnson presentation, I showed a few slides which I don’t think were given the time that they deserve. Not that we didn’t have enough time, I just don’t think I made as big a deal about them as I should have. Those particular slides demonstrated HTML Document Object manipulation executed in JavaScript outside any web browser. Those particular slides, and that particular code, is the culmination of over a year worth of work (and Yak Shaving) and I would like to talk about it a little more in detail here.
Since I started doing any sort of non-trivial browser dependent JavaScript, I’ve wanted to be able to test the code which I wrote. Hitting refresh on a webpage seems like a hack. Setting up a special browser to refresh the page for me also seems like a hack. I want to run “rake test” and have my JavaScript DOM manipulations tested right along with everything else, no browser dependence required. As far as I could tell, we need three things to make that happen:
- A JavaScript runtime that can be used in Ruby
- A parser with browser-like HTML correction schemes
- A DOM interface that mirrors a browsers DOM interface
Over the weekend, I think we’ve come a lot closer. John has finally released Johnson. Johnson solves problem number 1. Johnson provides a JavaScript runtime that is fully accessible in Ruby. Watch our RubyConf 2008 presentation about Johnson for more details about that project.
Number 2, I believe, has been solved by nokogiri. As far as I can tell, the tree generated inside libxml2 is very similar to one found in the browser. Nokogiri was partly a Yak Shave for number 3. Since I had writing a DOM interface in mind, nokogiri’s api lends well to writing a DOM api.
Number 3 was partly solved this weekend. I’ve been working on a DOM api called taka. Taka sits a DOM api on top of nokogiri. The goal of the project is to mirror a browser’s DOM api in Ruby.
With these three tools in place, I believe that we have a good start on a browserless JavaScript testing environment.
Codes
Enough talk. Let’s look at some codes. Take this HTML page for example:
<html>
<head>
<script>
function populateDropDown() {
var select = document.getElementById('colors');
var options = ['red', 'green', 'blue', 'black'];
var i;
for(i = 0; i < options.length; i++) {
var option = document.createElement('option');
option.appendChild(document.createTextNode(options[i]));
option.value = options[i];
select.appendChild(option);
}
}
</script>
</head>
<body onload="populateDropDown()">
<h1>Behold the Johnson</h1>
<form>
<select id="colors">
</select>
</form>
</body>
</html>
The JavaScript in this HTML will add a few option tags as children of the select tag. Effectively populating the drop down for our user. It would be nice if we could write a test to assert that when this JavaScript executes, the option tags are actually added as children of the select tag.
With Johnson and Taka, it is possible to write such a test:
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 27 28 29 30 31 32 33 |
require 'rubygems' require 'taka' require 'johnson' require 'test/unit' class OptionTagsAppendedTest < Test::Unit::TestCase def setup # Create our DOM object @document = Taka::DOM::HTML(DATA.read) # Create a new JavaScript runtime @rt = Johnson::Runtime.new # Set the document in the runtime @rt['document'] = @document # Execute any script tags @document.getElementsByTagName('script').each do |script| @rt.evaluate(script.textContent); end end def test_options_populated_by_onload # 0 option tags before onload is executed assert_equal 0, @document.getElementsByTagName('option').length # Execute the onload body attribute @rt.evaluate(@document.getElementsByTagName('body')[0].onload) # 4 option tags after onload is executed assert_equal 4, @document.getElementsByTagName('option').length end end |
There. It’s done. This test executes the JavaScript and manipulates your HTML the same way the browser would. You can run this code today, just make sure to install the johnson and taka gems first.
Problems
There are at least a few problems. This HTML code is, admittedly, carefully crafted. So far, taka only implements the DOM 1 interface. That means taka is missing many methods that are available in browsers. The good news though is that Taka is pure ruby and open source. As soon as you find methods that are missing, fork the repo, add a test, and send a pull request. I will be sure to merge it.
Conclusion
We are making progress towards testing JavaScript without a browser. We have to do it a step at a time. The solution I have presented to you, while not complete, has promise. I think that the only thing standing in our way right now is time and man power. The methods that need to be implemented on Taka to make it mirror a browser are not hard (take a look at the taka source). These methods just need to be written.