Tom MacWright

tom@macwright.com

Client-Side Javascript Testing & happen

Update: This post was published in 2011. As of 2017,
  • Happen still works, is still available, and is still useful for creating event objects. It's less necessary to test old IE versions, so more people should be comfortable with just using the Event object, instead of using an abstraction like Happen.

TL;DR - I wrote a library called Happen that makes it easier to use the createEvent API to do browser tests that actually use events.

A quick interlude of the land of testing.

For my projects recently, I’ve been using Jasmine for testing Wax and Modest Maps. 1

But the bugs that started hitting me weren’t the bugs I was testing against. Picky browser bugs as silly as Internet Explorer’s handling of window.setInterval(function(){}, 0) weren’t being tested for because all I was testing was the API, not the functioning of the thing. What I needed was browser events, and I found that there weren’t any options for getting them, so I wrote a very tiny one: Happen.

Happen lets Wax and Modest Maps do tests with user events. That means full-integration tests - not just testing that Modest Maps can figure out the URLs for tile images. With Happen, I can test that the browser can successfully move a map, reposition tiles, and fire events at the proper time when a user mouses down, drags, and mouseups - and the rest of the things a map should do in response to user input. And not by rewriting the API or wrapping these things any more - by actually providing the code with events that work just like normal events, because they are normal events.

This test is mapping bliss:

it('does not zoom in on single click', function() {
    expect(map.getZoom()).toEqual(0);
    happen.click(map.parent);
    expect(map.getZoom()).toEqual(0);
});

it('zooms in on double click', function() {
    expect(map.getZoom()).toEqual(0);
    happen.dblclick(map.parent);
    expect(map.getZoom()).toEqual(1);
});

The How and Why

The magic under the surface is document.createEvent and the initEvent APIs. They’re rather obtuse: here’s part of Happen’s abstraction code:

evt.initMouseEvent(o.type,
    true, // canBubble
    true, // cancelable
    window, // 'AbstractView'
    o.clicks || 0, // click count
    o.screenX || 0, // screenX
    o.screenY || 0, // screenY
    o.clientX || 0, // clientX
    o.clientY || 0, // clientY
    o.ctrl || 0, // ctrl
    o.alt || false, // alt
    o.shift || false, // shift
    o.meta || false, // meta
    o.button || false, // mouse button
    null // relatedTarget
);

Other Options

jQuery does have part of this API: you can call $('#thing').click(), but it operates on a different level, by attempting to find event listeners and then triggering them: the freeform jQuery event system is super-useful, but not something that’s actually in the DOM - it’s very well-orchestrated magic. What we’re creating here are real events with normal bubbling and normal default behavior.

So far this combo is what works: Selenium is the only alternative I’ve found that does real-life events, and I think that it solves the wrong problems: is it really that hard to initially write tests and run them in browers? That’s not a problem for smaller test suites - the problem is maintenance and being able to run your tests everywhere, that matters.

So, now I can open up the testing index.html in Modest Maps or Wax and it just works - no need to install tests on a Windows-running netbook or an iPad. So far it’s testing bliss. If you’ve got client-side libraries and loove testing, happen might help you out.