‹prev | 
My Chain | 
next›
I believe that I have more or less finished exploring testing 
Backbone.js with 
jasmine.  Ready to tackle something new, I think I will try adding a different view to my calendar application.  And what better way to add a new feature that driving it with BDD?  
And lo!  I know just the tool to BDD that feature.  Looks like I am not quite done with Jasmine after all…
For my month view, I have just been stubbing (with 
sinon.js) server lookups to return a single appointment.  For the list view, I think I ought to start by returning three appointments:
  describe("list view", function() {
    var couch_doc1 = {
          "_id": "1",
          "_rev": "1-1111",
          "title": "Appt 001",
          "startDate": "2011-10-01"
          // ...
        }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = {
          "total_rows": 3,
          "rows": [
            {"value": couch_doc2},
            {"value": couch_doc1},
            {"value": couch_doc3} ]
        };
My backend is, of course, 
CouchDB—hence the structure of the document list that will be returned.  I am intentionally mucking with the order of the documents because I am anticipating sorting them in the front-end.
With the appointment documents ready, I write my spec setup, which does the usual of creating a new instance of my Backbone 
Cal application.  It also stubs out server calls to return the document list from the main 
describe("list view", ...) block:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }
    beforeEach(function() {
      window.calendar = new Cal($('#calendar'));
      Backbone.history.navigate('#list', true);
      // populate appointments for this month
      server.respondWith('GET', /\/appointments/,
        [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
  });
The only real difference between this setup and the setup for my month view is that I am navigating to 
'#list' rather than 
'#month'.
On that list page, I expect to see 3 appointments.  Or, in jasmine-ese:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }
    beforeEach(function() { /* ... */ });
    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
When I load the new "list view" specs in the browser, I get the expected failure:
So my next question is, what do I need to do to change the failure message or make it pass?  Simply asking the question makes me realize I have gotten ahead of myself.  To do any of this, I am going to need a "list view".  I do not even have a route that responds to such a thing, let alone a view.
So, I take a step back and mark this test pending (for now) and write a more appropriate first step:
  describe("list view", function() {
    var /* couch_doc1, couch_doc2, couch_doc3, doc_list */
    beforeEach(function() { /* ... */ });
    it("routes to the list view", function() {
      expect($('h1')).toHaveText(/Appointment List/);
    });
    xit("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
With that, I have a smaller failure:
To be sure, I could make an even smaller first failure by testing the routing itself.  I am jumping ahead of myself a little—hopefully I will not get burned.  It turns out to require quite a few changes simply to get the new view to render.  I have to add a new route, which calls a new method on my application view, which creates a new view type:
  var Application = Backbone.View.extend({
    // ...
    setListView: function() {
      this.view = 'list';
      return this.render();
    },
    render: function() {
      var view;
      if (this.view == 'list') {
        view = new CalendarList();
      }
      else {
        var date = this.collection.getDate();
        view = new CalendarMonth({date: date});
      }
      $(this.el).html(view.render().el);
    },
    // ...
  });
  var CalendarList = Backbone.View.extend({
    render: function() {
      $(this.el).html("<h2>Appointment List</h2>");
      return this;
    }
  });
After all of that, I have my first passing test:
That took a bit more effort than anticipated.  I am pretty sure I have introduced a memory leak in there.  I 
know that I am violating at least one 
Recipes with Backbone recipe ("Evented Routers").  But none of that matters: (1) get it done, (2) do it right, (3) optimize.  I have reached #1 and that is good enough for now.  I will worry about #2 and #3 another day if needed (and with the benefit of more tests).
For now, I would like to get my original test passing:
    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
In the spirit of "get it done", I pull out an 
Underscore.js reduce():
    var CalendarList = Backbone.View.extend({
      initialize: function(options) {
        options.collection.bind('reset', this.render, this);
      },
      render: function() {
        $(this.el).html(
          '<h2>Appointment List</h2>' +
          '<ol id="appointment-list">' +
          this.collection.reduce(function(memo, appointment) {
            return memo + '<li>' + appointment.title + '</li>';
          }, "") +
          '<ol>'
        );
        return this;
      }
    });
To be sure, this is a very rudimentary listing of appointments.  In the final version, I will certainly need to show more than a list of static titles.  But it does enough to make my test pass:
Since it works, that will serve as a stopping point tonight.
Day #116