Ben Wainwright
Ben Wainwright

Reputation: 4611

Cannot get Karma/Jasmine to work with my angular controller

Ok. I have spent several hours trying in vain to get Karma to work with my Angular controller. Whatever I do, I get the following error. It seems that even if I remove the expectGET() calls, I still get the error; as soon as I call $http.flush();

TypeError: Cannot set property 'totalBeforeDiscounts' of undefined

The code for my controller is as follows:

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

quotePadControllers.controller('QuotesController', ['$scope', '$http', '$q', function($scope, $http, $q){

var blankAddon;

// Setup initial state and default values
var ajaxGetAddOns = $http.get('/?ajax=dbase&where=aons'),
    ajaxGetFrames = $http.get('/?ajax=dbase&where=fcats');

$q.all([ajaxGetAddOns, ajaxGetFrames]).then(function(results){

    $scope.addons = results[0].data;
    $scope.frames = results[1].data;

    $scope.pairs = [
        {
            "frames" : angular.copy($scope.frames),
            "addons" : angular.copy($scope.addons),
        }
    ];
});

// Function for the 'add pair' button
$scope.addPair = function()
{

    $scope.pairs.push({
            "frames" : angular.copy($scope.frames),
            "addons" : angular.copy($scope.addons)
    });
};

// Function for the 'remove pair' button
$scope.removePair = function()
{
    if ( $scope.pairs.length > 1 ) 
    {
        $scope.pairs.pop();
    }
};


// Continually update the subtotal and total
$scope.$watch('pairs', function(pairs) {
    var totalBeforeDiscounts = 0; 
    angular.forEach(pairs, function(pair) {
        var subTotal = 0;
        angular.forEach(pair.addons, function(addon) {
            subTotal += addon.added ? addon.price : 0;
        });

        subTotal += pair.currentFrame !== undefined ? pair.currentFrame.price : 0;
        pair.subTotal = subTotal;
        totalBeforeDiscounts += subTotal;
    });
    pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);
}]);

and my test code:

describe('QuotesController', function()
{
beforeEach(module('quotePadApp'));

var ctrl, $scope, $http, frameCatsHandler, addOnsHandler, createController;


// Setup tests
beforeEach(inject(function($controller, $rootScope, $httpBackend, _$q_) {

    $scope = $rootScope.$new();
    $http = $httpBackend;

    frameCatsResponse = [{"id":145,"price":25,"brand":"mybrand"},
                         {"id":147,"price":45,"brand":"mybrand"},
                         {"id":148,"price":69,"brand":"mybrand"}]; 

    addOnsHandler = [{"id":1,"name":"addon1","price":30,"includeIn241":0,"description":null},
                     {"id":2,"name":"addon2","price":60,"includeIn241":0,"description":null}];              

    frameCatsHandler = $http.when('GET', '/?ajax=dbase&where=fcats').respond(frameCatsResponse);
    addOnsHandler = $http.when('GET', '/?ajax=dbase&where=aons').respond(addOnsHandler);

    createController = function()
    {
        return $controller('QuotesController', {'$scope' : $scope });
    };
}));

it('Should request frame cats and addons from the database', function()
{
    $http.expectGET('/?ajax=dbase&where=aons');
    $http.expectGET('/?ajax=dbase&where=fcats');
    createController();
    $http.flush();
});
});

Upvotes: 1

Views: 140

Answers (1)

user2943490
user2943490

Reputation: 6940

This is because you have the following watch statement in your controller trying to set a totalBeforeDiscounts property on $scope.pairs.

$scope.$watch('pairs', function(pairs) {
    // ...
    pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);

In your tests, when you create the controller and then call $http.flush(), that's actually triggering a $digest cycle. This kicks off all watchers.

createController();
$http.flush();

The watch handler above will execute and since it executes before $scope.pairs has any value, the pairs argument passed into the watch handler is undefined, resulting in your error.

As per the documentation:

After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization. https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch

Also, in the rest of your code you have $scope.pairs as an array, but in the watch you are trying to set a property like totalBeforeDiscounts on it. This doesn't look right.

Upvotes: 1

Related Questions