Search code examples
backbone.jsrequirejsjasmine

Backbone Views / Jasmine / Requirejs - Cannot read property replace of undefined


Using Jasmine, I have already written tests for my models and controllers and all have passed. However, I am running into a lot of problems if I test my views.

  1. IF I remove views/hf_appView from the define function in base.js, the PostViewSpec test will pass. This is just a very simple and contrived example, but it illustrates an error I keep getting (in the developer tools console of my browser) when trying to test other views as well.

  2. See the second example below in which I can't get to work at all. I did this to see if a different view module would work given there might be something wrong with the base module, but no luck.

  3. Even in the passing collection spec, I then added a reference to views/hf_postView in the define function just to see what happens to the test and then all of a sudden I get the same error as #1 & 2 noted below.

Based on the errors below, one theory I have is that: Perhaps the #listTpl that is attached to the view is not passed through because the DOM isn't ready as the view gets parsed? If so, how can I fix this?

Again, the models and controllers pass, but once I start including the view modules I am having a lot trouble. The error is:

Error in Chrome

Cannot read property 'replace' of undefined

Error in Safari

[Warning] Invalid CSS property declaration at: * (jasmine.css, line 16)
[Error] TypeError: undefined is not an object (evaluating 'n.replace')
    template (underscore-min.js, line 5)
    (anonymous function) (hf_postView.js, line 12)
    execCb (require.js, line 1658)
    check (require.js, line 874)
    (anonymous function) (require.js, line 624)
    each (require.js, line 57)
    breakCycle (require.js, line 613)
    (anonymous function) (require.js, line 626)
    each (require.js, line 57)
    breakCycle (require.js, line 613)
    (anonymous function) (require.js, line 626)
    each (require.js, line 57)
    breakCycle (require.js, line 613)
    (anonymous function) (require.js, line 626)
    each (require.js, line 57)
    breakCycle (require.js, line 613)
    (anonymous function) (require.js, line 626)
    each (require.js, line 57)
    breakCycle (require.js, line 613)
    (anonymous function) (require.js, line 700)
    each (require.js, line 57)
    checkLoaded (require.js, line 699)
    completeLoad (require.js, line 1576)
    onScriptLoad (require.js, line 1679)

The 'anonymous function it is referencing in line 12 of hf_postView.js is

template: _.template($('#listTpl').html()), 

--------------------------------------

SpecRunner.html

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Jasmine Test Runner</title>
  <link rel="stylesheet" type="text/css" href="../../test/jasmine/lib/jasmine.css">
</head>
<body>

SpecRunner.js - Partial!!

specs.push('spec/PostModelSpec');
specs.push('spec/PostCollectionSpec');
specs.push('spec/PostViewSpec');

require(['boot'], function(){
  require(specs, function(){
    window.onload();
  });
});

PostViewSpec.js

define(['views/base'], function(Base) {

  describe("View :: PostView", function() {
    it('View :: Should have tag name', function() {
      var base = new Base();
      base.render()
      expect(base.tagName).toEqual('div');
    });

  }); //describe
});//define

views/base.js

define([
  'jquery',
  'underscore',
  'backbone',
  'views/hf_appView', //****REMOVE this and the hfAppView below, and the test passes ******//
  'collections/hf_collections'], function($, _, Backbone, hfAppView, hfposts){

    return Base = Backbone.View.extend({
      el: '#securities',

      render: function() {
         //add code here
      }
    });

});

--------------------------------------------------

PostViewSpec.js - Second Test Example - THIS DOESNT WORK AT ALL using a different view module.

define(['jquery', 'underscore', 'backbone', 'views/hf_postView'], function($, _, Backbone, hfPostView) {

    it('View :: Should have tag name', function() {
      var base = new hfPostView();
      base.render()
      expect(base.tagName).toEqual('li');
    });

  }); //describe
});//define

views/hf_postView

define([
  'jquery',
  'underscore',
  'backbone',
  'models/hf_models',
  'views/hf_appView',
  'utils'], function($, _, Backbone, PostModel, hfAppView, utils){

    return hfPostView = Backbone.View.extend({
      tagName: 'li',
      className: 'securities',
      template: _.template($('#listTpl').html()),
      events: {
        'click .delete': 'deletePost'
      },

      initialize: function() {
        this.listenTo(this.model, 'destroy', this.remove);
        if(!this.model) {
          throw new Error('Must have HFPOST model');
        }
      },

      render: function() {
        this.$el.html(this.template(   
          this.model.toJSON()
        ));
        return this; 
      },

      deletePost: function() {
        var confirmed = confirm("Are you sure?");
        if (confirmed) {
          this.model.destroy();
          utils.showAlert('Entry was deleted', '', 'alert-warning');
        }
      } //delete post
    }); //Backbone View
});

--------------------------------------------------

PostCollectionSpec.js - THIS works only if I remove the references to the hf_postView as well.

define(['collections/hf_collections', 'views/hf_postView'], function(hfposts, hfPostView) {

  describe('Controller :: Post Controller', function () {
    var posts;
    beforeEach(function() {
      posts = hfposts;
    });

    it('url should be /api', function() {
      expect(posts.url).toEqual('/api');
    });

    it('should create new model', function() {
      var post1 = hfposts.set({name: 'thisPost'});
      expect(post1.get("name")).toEqual('thisPost');
    });

  }); //describe
}); //define

Solution

  • This will happen if you pass _.template(...) an empty DOM element. With un-minified underscore.js, doing this ...

    _.template($('#thisDoesNotExist'), { foo: "bar" });
    

    ... leads to ...

     Uncaught TypeError: Cannot read property 'replace' of null underscore.js:1304 _.template
    

    Line 1304 of underscore.js is doing this:

    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
        source += text.slice(index, offset).replace(escaper, escapeChar);
        // ... and so on
    

    Your error of attempting to evaluate n.replace is the minified version of that text.replace(...) call.

    It happens because your unit test of the View is attempting to load something from the page that (I assume) is outside of the view itself via $('#listTpl'). The DOM element with ID "listTpl" does not exist on the page that is hosting the unit tests, so it returns nothing. Bummer.

    How to fix it? Since you're already using RequireJS, I suggest you also use the text plugin. This would allow you to define your template files wholly outside of the pages that are hosting the views in the application, which is darn nice for testing. It's also really easy to get running in an existing RequireJS app, as there is no extra download or configuration; just use the text! prefix for the dependency you want.

    Once you've moved your template out to a separate file, your View might look something like this:

    define([
        'jquery',
        'underscore',
        'backbone',
        'models/hf_models',
        'views/hf_appView',
        // Assume you created a separate directory for templates ...
        'text!templates/list.template',
        'utils'], function($, _, Backbone, PostModel, hfAppView, listTemplate, utils){
    
            return hfPostView = Backbone.View.extend({
                tagName: 'li',
                className: 'securities',
                // Woohoo!  The text plugin provides our template content!
                template: _.template(listTemplate),
                events: {
                    'click .delete': 'deletePost'
                },
            // ... and so on ...
    

    After that, your unit test does not have to change. Just requiring in views/hf_postView will automatically bring in the template text & any other dependencies.

    For good measure: I'm a big fan of testing Backbone Views by asserting stuff as close to what the real application will see as possible. Some of my View tests look like:

    describe('after the View has rendered', function() {
        var view;
    
        beforeEach(function() {
            view = new MyView({
                model: mockModel
            });
            view.render();
        });
    
        it('initially has a "Submit" button', function() {
            var content = view.$el.html();
            var matched = content.indexOf('<button>Submit</button>');
            expect(match).to.be.greaterThan(-1);
        });
    });
    

    Not exactly that, but you get the idea.