Monday, November 14, 2011

Jasmine Tests and Backbone.js Singleton Views

‹prev | My Chain | next›

I am down to two failing jasmine specs in my Backbone.js calendar application:


The click event spec is quite simple. I simulate a click on an appointment in the calendar, which I expect will display the edit dialog:

    it("binds click events on the appointment to an edit dialog", function() {
      $('.appointment', '#' + fifteenth).click();
      expect($('#edit-dialog')).toBeVisible();
    });
So why would the dialog not show? Perhaps the appointment was not there in the first place to be clicked. Checking firebug's view of the appointment, it is, however, in the DOM:


Looking at the view code, I notice that the events are not actually bound to the "appointment" class:
    var Appointment = Backbone.View.extend({
      template: template(
        '<span class="appointment" title="{{ description }}">' +
        '  <span class="title">{{title}}</span>' +
        '  <span class="delete">X</span>' +
        '</span>'
      ),
      // ...
      events: {
        'click .title': 'handleEdit',
        'click .delete': 'handleDelete'
      },
      // ...
    });
Ah, well that is easy enough. I simply need to switch my click target from the element with the "appointment" class to the element with the "title" class:

    it("binds click events on the appointment to an edit dialog", function() {
      $('.title', '#' + fifteenth).click();
      expect($('#edit-dialog')).toBeVisible();
    });
The other failing test is a rather important one, verifying that saving an appointment from a dialog results in a changed appointment in the calendar. It is almost a round trip test except that I use sinon.js to stub out the network request.

The appointment edit view is a singleton view. The singleton view makes sense since I am attaching it to a single jQuery UI dialog. Whenever an appointment is edited, I reset the dialog view to link to that appointment's model:
    var AppointmentEdit = new (Backbone.View.extend({
      reset: function(options) {
        this.model = options.model;
        this.render();
      },
      render: function () { /* ... */ },
      events : {
        'click .ok': 'update',
        // ... 
      },
      // ...
      update: function() {
        var options = {
          title: $('.title', '#edit-dialog').val(),
          description: $('.description', '#edit-dialog').val()
        };
        this.model.save(options);
      }
    }));
I have not had any problems with this dialog since I first implemented it, so I am somewhat perplexed that the spec is failing. Checking the javascript console, I find that the this.model.save() call in update() is failing because this.model is undefined. I find this quite bizarre because the events attribute should be taking care of ensuring that the this variable refers to the object that I just reset to link to the proper model. Quite perplexing.

When perplexed, I resort to console.log() debugging. I log the values of this and this.model. And, since this.model sometimes seems to be undefined, I add a guard clause to update() to prevent crashing:
    var AppointmentEdit = new (Backbone.View.extend({
      reset: function(options) {
        this.model = options.model;
        console.log(this);
        console.log(this.model);
        this.render();
      },
      render: function () { /* ... */ },
      events : {
        'click .ok': 'update',
        // ... 
      },
      // ...
      update: function() {
        console.log(this);
        if (!this.model) return;

        console.log(this.model);

        var options = {
          title: $('.title', '#edit-dialog').val(),
          description: $('.description', '#edit-dialog').val()
        };
        this.model.save(options);
      }
    }));
Re-running the specs, I find:


Ugh. Each subsequent loading of the application's fixture page is creating a new application instance, which creates a new singleton instance of the edit dialog. The previous dialog views are no longer attached to models, but they linger. Lingering, they still respond to the edit dialog's OK button (it is a jQuery delegated event). Without a model, the old singleon views crash.

Except now they do not—because of the guard clause that I added to aid in debugging. Now those lingering singleton views safely ignore the click events, allowing only the most recent to respond successfully.

And with that, I have my tests passing:


Up tomorrow: possibly trying to come up with a better solution to this or moving onto to add a few new tests.


Day #205

No comments:

Post a Comment