Unit testing a directive whose templates are all one with file with script tags

All we need is an easy explanation of the problem, so here it is.

I am having a hard time figuring out how to include my directive’s templates (that are all in one file in different script tags) in my Karma unit tests.

The error I get:

PhantomJS 1.9 (Linux) ERROR
 SyntaxError: Parse error
 at /var/www/html/tweak/core/global/views/js/modules/datable/templates.html:1
PhantomJS 1.9 (Linux): Executed 0 of 0 ERROR (0.313 secs / 0 secs)

Here are the relevant parts of the code:

My directives meat:

return {
  scope       : {
    columns : '=',
    config  : '='
  },
  templateUrl : 'datable/table.html',
  restrict    : 'E',
  controller  : 'datableCtrl',
  link        : linkingFunction
};

My template file:

<script type="text/ng-template" id="datable/table.html">
  <!-- data rows -->
  <tr
    ng-repeat="row in rows track by $id($index)"
    class="datable-row"
    ng-hide="loading">

    <td
      ng-repeat="column in columns track by $id($index)"
      ng-class="{'edit-on': editMode == 'on'}"
      class="{{column.classes.join(' ') + ' column' + $index}}"
      ng-style="column.style">

      <div ng-include="editMode == 'on' && column.editable
        ? 'datable/editCell.html'
        : 'datable/normalCell.html'">
      </div>
    </td>

    <!-- save button -->
    <td ng-show="editMode == 'on'" style="width:1px;">
      <button class="btn"> Save </button>
    </td>
    <!-- / save button -->

  </tr>
  <!-- / data rows -->
</script>

<script type="text/ng-template" id="datable/editCell.html">
  <div ng-switch="column.inputType">

    <!-- text input -->
    <div ng-switch-when="text">
      <div ng-class="{
        'input-append'  : column.append != '',
        'input-prepend' : column.prepend != ''
      }">

        <span
          class="add-on"
          ng-show="column.prepend"> {{column.prepend}} </span>

        <input
          type="text"
          ng-model="row[column.model]"
          ng-keydown="query()"
          ng-class="inputClass.join(' ')"
          ng-attrs="column.inputAttrs">

        <span
          class="add-on"
          ng-show="column.append"> {{column.append}} </span>
      </div>
    </div>
    <!-- end text input -->

    <!-- select input -->
    <div ng-switch-when="select">
      <select
        ng-model="row[column.model]"
        ng-change="query()"
        ng-options="item.value as item.name for item in column.options"
        ng-class="inputClass.join(' ')"
        ng-attrs="column.inputAttrs">

        <option value=""> -- </option>
      </select>
    </div>
    <!-- end select -->

    <!-- radio / checkbox -->
    <div ng-switch-default>
      <label ng-repeat="(key, value) in column.options track by $id($index)">
        <input
          type="{{column.inputType}}"
          ng-class="inputClass.join(' ')"
          ng-change="query()"
          value="{{key}}"
          ng-checked="row[column.model].indexOf('key') > -1"
          ng-attrs="column.inputAttrs">

        <span> {{value}} </span>
      </label>
    </div>
    <!-- end radio / checkbox -->

  </div>
</script>

<script type="text/ng-template" id="datable/normalCell.html">
  <div class="read-only">
    <span> {{column.prepend}} </span>
    <!-- <span> {{row[column.model] | datableFilter : column.filter}} </span> -->
    <span ng-bind-html-unsafe="(row[column.model] + '') | datableFilter : column.filter"></span>
    <span> {{column.append}} </span>
  </div>
</script>

My unit tests:

'use strict'

describe("datable", function() {

  describe('directive', function () {
    var $rootScope, $compile, element;

    beforeEach(module('datable'));
    beforeEach(module('/var/www/html/tweak/core/global/views/js/modules/datable/templates.html'));

    beforeEach(inject(function (_$rootScope_, _$compile_) {
      $rootScope = _$rootScope_;
      $compile = _$compile_;

      $rootScope.tableConfig = {
        editable     : true
      };
      $rootScope.columns = [];

      element = angular.element('<datable config="tableConfig" columns="columns"></datable>');

        $compile(element)($rootScope);
        $rootScope.$digest();
    }));

    it('should have ng-scope class', function() {
        expect(element.hasClass('ng-scope')).toBe(true);
    });
  });
});

My Karma config:

var branch = 'tweak';
basePath = '/var/www/html/' + branch + '/';

files = [
  // Dependencies
  JASMINE,
  JASMINE_ADAPTER,
  'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js',
  'https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js',
  'http://code.angularjs.org/1.1.5/angular-mocks.js',

  // other requirements
  'core/global/views/js/modules/rest/module.js',

  // the project source
  'core/global/views/js/modules/datable/module.js',
  'core/global/views/js/modules/datable/values.js',
  'core/global/views/js/modules/datable/services.js',
  'core/global/views/js/modules/datable/filters.js',
  'core/global/views/js/modules/datable/directives.js',
  'core/global/views/js/modules/datable/controllers.js',
  'core/global/views/js/modules/datable/*.html',

  // my spec suite
  'core/global/views/js/modules/datable/tests.js'
];

exclude = [

];

reporters = ['progress'];
port = 9876;
runnerPort = 9100;
colors = true;
logLevel = LOG_INFO;
autoWatch = true;
browsers = ['PhantomJS'];
captureTimeout = 60000;

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

I think your error is because you are trying to load your HTML file into a file list that normally accepts javascript. I do have a solution for you though.

Before I begin, I have karma 0.10.2 and it looks like you are on 0.8.x or under? I have this working in 0.10.2 but I can’t install 0.8.x. I’ll try to translate for 0.8.x but won’t be able to test what I’m doing, so I’ll describe mainly in terms of 0.10.x. It may be easier to move to latest karma anyway if you are able.

Config

0.10.x

External HTML partials can be loaded by karma-ng-html2js-preprocessor. This is normally used for loading directly in directives via templateUrl and similar methods. In 0.10.2 you need to make sure this package is installed (using npm) and then include the following in your karma config:

preprocessors: {
    '**/*.html' : ['ng-html2js']
},

ngHtml2JsPreprocessor: {
    cacheIdFromPath: function(filepath) {
        // If you had more than one html file you would want to do something more clever here.
        return 'inlinetemplates';
    },
    moduleName: 'inlinetemplates'
},

plugins: [
    ...,
    'karma-ng-html2js-preprocessor'
],

files: [
    ...,
    'app/alltemplates.html', // your main template html
    // Don't include paths for individual files that are inlined in the file above
]

This will allow you to load a module with module('inlinetemplates') that will insert the contents of your main template file (not the individual templates) into $templateCache.

0.8.x

So, translating for 0.8.x… I think you need to use html2js which is not so powerful but is included in karma in this version. You won’t need to install or include it in plugins, and you can’t configure how it’s used, so you just need

preprocessors = { '**/*.html': ['html2js'] }

The module created, and the item it inserts into $templateCache will be named using the path that you use to refer to your main template html.

Javascript

0.10.x

Now you should be able to load the relevant module and get access to the contents of your main template file using

var templates = $templateCache.get('inlinetemplates')

All that is left to do is pushing your inlined templates from the main template file contents to $templateCache. This is done using the angular script directive, so we just need to compile/link the file we have loaded with angular. You can do this very simply with

$compile(templates)(scope);

So putting this together, you can include the following in any describe block that needs to load your templates.

beforeEach(module('inlinetemplates'));
beforeEach(inject(function($compile, $templateCache, $rootScope) {
    var templatesHTML = $templateCache.get('inlinetemplates');
    $compile(templatesHTML)($rootScope);
}));

0.8.x

var mainTemplateLocation = 'path/used/to/refer/to/main/templates/in/karma/conf.html';
beforeEach(module(mainTemplateLocation));
beforeEach(inject(function($compile, $templateCache, $rootScope) {
    var templatesHTML = $templateCache.get(mainTemplateLocation);
    $compile(templatesHTML)($rootScope);
}));

Summing Up

Again, I can’t guarantee that the 0.8.x instructions will work, especially not without tweaking, but this certainly works in 0.10.x.

Karma already has the facilities for pushing external HTML partials into your tests, all that was missing was being able to interpret your main template properly.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply