Unit testing in AngularJS: Initializing the controller scope of a directive

Here is the code for a directive with a separate controller using the "controller as" syntax:

'use strict';

angular.module('directives.featuredTable', [])

.controller('FeaturedTableCtrl',
['$scope',
function ($scope){
  var controller = this;

  controller.activePage = 1;
  controller.changePaginationCallback =
    controller.changePaginationCallback || function(){};
  controller.density = 10;
  controller.itemsArray = controller.itemsArray || [];
  controller.metadataArray = controller.metadataArray || [];
  controller.numberOfItems = controller.numberOfItems || 0;
  controller.numberOfPages = 1;
  controller.options = controller.options || {
    'pagination': false
  };

  controller.changePaginationDensity = function(){
    controller.activePage = 1;
    controller.numberOfPages =
      computeNumberOfPages(controller.numberOfItems, controller.density);

    controller.changePaginationCallback({
      'page': controller.activePage,
      'perPage': controller.density
    });
  };

  controller.getProperty = function(object, propertyName) {
    var parts = propertyName.split('.');

    for (var i = 0 ; i < parts.length; i++){
      object = object[parts[i]];
    }

    return object;
  };

  controller.setActivePage = function(newActivePage){
    if(newActivePage !== controller.activePage &&
      newActivePage >= 1 && newActivePage <= controller.numberOfPages){

      controller.activePage = newActivePage;
      controller.changePaginationCallback({
        'page': controller.activePage,
        'perPage': controller.density
      });
    }
  };

  initialize();

  $scope.$watch(function () {
    return controller.numberOfItems;
  }, function () {
    controller.numberOfPages =
      computeNumberOfPages(controller.numberOfItems, controller.density);
  });

  function computeNumberOfPages(numberOfItems, density){
    var ceilPage = Math.ceil(numberOfItems / density);
    return ceilPage !== 0 ? ceilPage : 1;
  }

  function initialize(){
    if(controller.options.pagination){
      console.log('paginate');
      controller.changePaginationCallback({
        'page': controller.activePage,
        'perPage': controller.density
      });
    }
  }
}]
)

.directive('featuredTable', [function() {
return {
  'restrict': 'E',
  'scope': {
    'metadataArray': '=',
    'itemsArray': '=',
    'options': '=',
    'numberOfItems': '=',
    'changePaginationCallback': '&'
  },
  'controller': 'FeaturedTableCtrl',
  'bindToController': true,
  'controllerAs': 'featuredTable',
  'templateUrl': 'directives/featuredTable/featuredTable.tpl.html'
};
}]);

The controller initializes its properties at the start based on the attributes passed by the directive or defaults:

controller.activePage = 1;
controller.changePaginationCallback =
    controller.changePaginationCallback || function(){};
controller.density = 10;
controller.itemsArray = controller.itemsArray || [];
controller.metadataArray = controller.metadataArray || [];
controller.numberOfItems = controller.numberOfItems || 0;
controller.numberOfPages = 1;
controller.options = controller.options || {
  'pagination': false
};

At the end of the controller, the initialize(); function is executed to handle callbacks according to the options:

function initialize(){
  if(controller.options.pagination){
    controller.changePaginationCallback({
      'page': controller.activePage,
      'perPage': controller.density
    });
  }
}

For unit testing the controller (using karma and jasmine), an attempt was made to simulate the directive's parameters:

'use strict';

describe('Controller: featured table', function () {

  beforeEach(module('directives.featuredTable'));

  var scope;
  var featuredTable;
  var createCtrlFn;
  beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();

    createCtrlFn = function(){
      featuredTable = $controller('FeaturedTableCtrl', {
        '$scope': scope
      });
      scope.$digest();
    };
  }));

  it('should initialize controller', function () {
    createCtrlFn();

    expect(featuredTable.activePage).toEqual(1);
    expect(featuredTable.changePaginationCallback)
      .toEqual(jasmine.any(Function));
    expect(featuredTable.density).toEqual(10);
    expect(featuredTable.itemsArray).toEqual([]);
    expect(featuredTable.metadataArray).toEqual([]);
    expect(featuredTable.numberOfPages).toEqual(1);
    expect(featuredTable.numberOfItems).toEqual(0);
    expect(featuredTable.options).toEqual({
      'pagination': false
    });
  });

  it('should initialize controller with pagination', function () {
    scope.changePaginationCallback = function(){};
    spyOn(scope, 'changePaginationCallback').and.callThrough();

    scope.options = {
      'pagination': true
    };

    createCtrlFn();

    expect(featuredTable.activePage).toEqual(1);
    expect(featuredTable.changePaginationCallback)
      .toEqual(jasmine.any(Function));
    expect(featuredTable.density).toEqual(10);
    expect(featuredTable.itemsArray).toEqual([]);
    expect(featuredTable.metadataArray).toEqual([]);
    expect(featuredTable.numberOfPages).toEqual(1);
    expect(featuredTable.numberOfItems).toEqual(0);
    expect(featuredTable.options).toEqual({
      'pagination': true
    });

    expect(featuredTable.changePaginationCallback).toHaveBeenCalledWith({
      'page': 1,
      'perPage': 10
   });
  });
});

An error occurred during testing indicating that the scope was not properly initialized:
Expected Object({ pagination: false }) to equal Object({ pagination: true })
at test/spec/app/rightPanel/readView/historyTab/historyTab.controller.spec.js:56

Answer №1

Replicating the functionality of the bindings in a directive can be quite challenging - it's difficult to precisely understand how compiling and linking a directive interacts with the data it receives...unless you decide to take matters into your own hands!

The angular.js documentation provides a helpful walkthrough on how to compile and link a directive for unit testing - https://docs.angularjs.org/guide/unit-testing#testing-directives. Once you follow those steps, all you need to do is extract the controller from the resulting element(consult the documentation for the controller() method here - https://docs.angularjs.org/api/ng/function/angular.element) and conduct your tests. In this scenario, using ControllerAs is unnecessary - the focus is on directly testing the controller instead of manipulating the scope.

Take a look at this sample module:

var app = angular.module('plunker', []);

app.controller('FooCtrl', function($scope) {
  var ctrl = this;

  ctrl.concatFoo = function () {
    return ctrl.foo + ' world'
  }
})
app.directive('foo', function () {
  return {
    scope: {
      foo: '@'
    },
    controller: 'FooCtrl',
    controllerAs: 'blah',
    bindToController: true,
  }
})

And here's the setup for testing:

describe('Testing a Hello World controller', function() {
  ctrl = null;

  //remember to specify your module in the test
  beforeEach(module('plunker'));

  beforeEach(inject(function($rootScope, $compile) {
    var $scope = $rootScope.$new();
    var template = '<div foo="hello"></div>'
    var element = $compile(template)($scope)

    ctrl = element.controller('foo')

  }));

  it('should generate hello world', function() {
    expect(ctrl.concatFoo()).toEqual('hello world')
  });
});

(Interactive demo: http://plnkr.co/edit/xoGv9q2vkmilHKAKCwFJ?p=preview)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Using Mongoose to push objects into an array in a Node.js environment

I am faced with a peculiar issue involving an array of images in my code. Here is the structure: var newImageParams = [ { image_string: "hello", _type: "NORMAL" }, { image_string: "hello", _type: "NORMAL" } ] M ...

What is the method to retrieve response text?

This is the content of my register.js file: var formdata = new FormData(); formdata.append("name", name.value); formdata.append("username", username.value); formdata.append("email", email.value); formdata.append("password", password.value) ...

The Facebook Comments feature on my React/Node.js app is only visible after the page is refreshed

I am struggling with getting the Facebook Comment widget to display in real-time on my React application. Currently, it only shows up when the page is refreshed, which is not ideal for user engagement. Is there a way to make it work through server-side r ...

Error in Typescript: The property 'children' is not included in the type but is necessary in the 'CommonProps' type definition

Encountering this error for the first time, so please bear with me. While working on a project, I opened a file to make a change. However, instead of actually making any changes, I simply formatted the file using Prettier. Immediately after formatting, t ...

Using Javascript to Showcase a Video's Number of Views with Brightcove

How can I show the number of views for a video without using Brightcove's player? Brightcove Support shared this resource: , but I'm having trouble understanding it. ...

Incorporate a unique identifier $id within the ajax request

Whenever I make changes to my product, I would like the categories to show up in a dropdown menu. Currently, it only displays: -- Select your category -- For instance, if my $current_category_id = 2, I want the dropdown to show the relevant categories. ...

Spacing issues while utilizing the <textarea> element

<tr> <td> <b>Escalation: </td></b> <td> <TextArea name='escalation' onKeyDown=\"limitText(this.form.escalation,this.form.countdown,100);\" onKeyUp=\"limitText ...

Automatic capitalization feature for input fields implemented with AngularJS

I have integrated a directive into my input field to validate a license key against a server-side API. While this functionality works well, I also want the license key to automatically add hyphens and appear in capital letters. For example, when a user in ...

++first it must decrease before it increases

I'm attempting to create a basic counter feature, where clicking on a button labelled "+" should increase a variable named Score by 1, and the "-" button should decrease it by 1. However, I've encountered an issue where pressing the "+" button fo ...

Arrange the grid in a pleasing manner

I'm really struggling with this issue. In my current setup, I have a Grid container that holds two separate grids - one for a textfield and another for a checkbox. Unfortunately, I can't seem to get them to align properly. <Grid container& ...

Assigning ng-model within a nested ng-repeat to use as parameters in an ajax request

How can I make an ajax call using ng-model values as parameters in my AngularJS project? Is it allowed to set ng-model to json data directly? Also, how should I set the scope for ng-model in the controller? Controller EZlearn.controller("testController", ...

Building Silent Authentication in React Native with the help of Auth0: A Step-by-Step Guide

I am currently working on my first React Native app, and I have integrated Auth0 for authentication purposes. My goal is to implement silent authentication using refresh tokens. So far, I have attempted to use the checkSession() method but encountered an ...

JavaScript/CSS memory matching game

Just starting out in the world of programming and attempting to create a memory game. I've designed 5 unique flags using CSS that I want to use in my game, but I'm feeling a bit stuck with where to go next. I understand that I need some function ...

Utilizing Django models to populate Google Charts with data

I am currently working on showcasing charts that display the most popular items in our store. To achieve this, I need to extract data from the database and incorporate it into the HTML. Unfortunately, the charts are not loading as expected. When testing wi ...

What steps should I take to resolve the issue with the error message: "BREAKING CHANGE: webpack < 5 previously included polyfills for node.js core modules by default"?

When attempting to create a react app, I encounter an issue every time I run npm start. The message I receive is as follows: Module not found: Error: Can't resolve 'buffer' in '/Users/abdus/Documents/GitHub/keywords-tracker/node_modul ...

Is it a mistake to gather errors together and send them to the promise resolve in JavaScript?

When processing a list in a loop that runs asynchronously and returns a promise, exiting processing on exception is not desired. Instead, the errors are aggregated and passed to the resolve callback in an outer finally block. I am curious if this approach ...

Spinning Loader in ui-select AngularJS

Currently, I am working with AngularJs and the ui-select plugin from ui-select. My goal is to implement a spinner while fetching data from the server. How can I integrate a spinner directly into the HTML code? The following snippet shows the existing HTML ...

Display the list in a grid format with 4 columns and x number of rows by utilizing Angular and Bootstrap

I need to display a list of values [1,2,3,4,5,6,7,8,9,10] as a bootstrap grid (class="col-x") using AngularJS. My goal is to create a grid with 4 columns and 3 rows from this list. Can you suggest the most efficient method to achieve this? ...

Creating an array of objects sorted in alphabetical order

My task involves working with an array of objects that each have a name property: var myList = [{ name: 'Apple' }, { name: 'Nervousness', }, { name: 'Dry' }, { name: 'Assign' }, { name: 'Date' }] ...

I am interested in retrieving a variable from a function

this is some unique HTML code <form id="upload"> <label for="file">Choose a File to Upload</label> <input type="file" id="file" accept=".json"> <butto ...