Archive

Archive for January, 2011

Testing events on jQuery objects with Jasmine

January 10, 2011 5 comments

Recently we had a piece of JavaScript code that looked roughly like this:

myApp.buttonBinder = {
  bind: function(button) {
    if (myApp.shouldChangeButtonAction()) {
      button.unbind("click").click(function() {
        button.closest("form").submit();
      });
    }
  }
}

So we wanted to test this using Jasmine.
Our main goal was to test that when the button was clicked the form that contains it was being submitted.
It would be cool if we could use Jasmine’s spies:

describe("myApp.buttonBinder", function() {
  it("should bind form submit to button", function() {
    var form = $("<form/>");
    var button = $("<input/>");
    form.append(button);
    spyOn(form, 'submit');

    myApp.buttonBinder.bind(button);
    button.click();

    expect(form.submit).toHaveBeenCalled();
  });
});

But unfortunately this does not work. I didn’t look too much into it, but I suspect it is due to the fact that when the actual code runs, the jQuery ‘closest’ selector creates a new form object that represents the same DOM element. But as it is a different object the spy can’t recognize that the function submit has been called.
One idea is to instead add a listener to the form’s submit event and check if this listener has been called:

describe("myApp.buttonBinder", function() {
  it("should bind form submit to button", function() {
    var form = $("<form/>");
    var button = $("<input/>");
    form.append(button);
    var formHasBeenSubmitted = false;
    form.submit(function() {
      formHasBeenSubmitted = true;
    });

    myApp.buttonBinder.bind(button);
    button.click();

    expect(formHasBeenSubmitted).toBeTruthy();
  });
});

And voilá! This works!
But it looks terrible, doesn’t it?
Good thing Jasmine lets us extend itself, so we can create a new spy function and a new custom matchers:

// SpecHelper.js
var jasmineExtensions = {
  jQuerySpies: {},
  spyOnEvent: function(element, eventName) {
    var control = {
      triggered: false
    };
    element.bind(eventName, function() {
      control.triggered = true;
    });
    jasmineExtensions.jQuerySpies[element[eventName]] = control;
  };
};

var spyOnEvent = jasmineExtensions.spyOnEvent;

beforeEach(function() {
  this.addMatchers({
    toHaveBeenTriggered: function() {
      var control = jasmineExtensions.jQuerySpies[this.actual];
      return control.triggered;
    }
  });
});

And now our test looks like this:

describe("myApp.buttonBinder", function() {
  it("should bind form submit to button", function() {
    var form = $("<form/>");
    var button = $("<input/>");
    form.append(button);
    spyOnEvent(form, 'submit');

    myApp.buttonBinder.bind(button);
    button.click();

    expect(form.submit).toHaveBeenTriggered();
  });
});

Much better \o/

Follow

Get every new post delivered to your Inbox.