Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test changes on Component Bindings by parent element?

I have a component like follows and would like to test what the $onChange method does in case the binding myBinding changes.

I tried the whole morning, but could not find a way to solve this.

angular
    .module('project.myComponent', [])
    .component('myComponent', {
        bindings: {
            myBinding: '<'
        },
        template: '<div>{{$ctrl.result}}</div>',
        controller: myComponentController
    });

function myComponentController($filter, someService) {
    var ctrl = this;
    ctrl.result = 0;

    $ctrl.$onChange = function (changes) {
        if(angular.isDefined(changes.myBinding)) {
            if(angular.isDefined(changes.myBinding.currentValue)) {
                if(angular.isDefined(changes.myBinding.currentValue != changes.myBinding.previousValue)) {
                    myService.doSomething(changes.myBinding.currentValue).then(
                        function(data) {
                            ctrl.result = changes.myBinding.currentValue * 3;
                        }
                    );                  
                }
            }
        }
    }
}

I would like my test acting like it is the components parent which changes the value of the binding.

require('angular-mocks');

describe('myComponment', function() {
    var element, scope;

    beforeEach(inject(function(_$rootScope_, _$compile_) {

    }));

    fit('should display the controller defined title', function() {        
        // prepare test and set myBinding to 10
        expect(component.result).toBe(30);
    });
});

Is that possible? How? Any hints? Plunker, CodePen or other examples?

like image 228
n00n Avatar asked Jan 04 '18 18:01

n00n


People also ask

What is the correct way to trigger a click event on a button when testing an Angular component?

For click event we can use triggerEventHandler method of Angular DebugElement class. We can also call native JavaScript click method of button. On click of button, we call a component method and it is possible that our component method has other dependencies to execute.

What is TestBed in Angular testing?

TestBed is the primary api for writing unit tests for Angular applications and libraries.

What is ComponentFixture Jasmine?

The ComponentFixture is a test harness for interacting with the created component and its corresponding element. Access the component instance through the fixture and confirm it exists with a Jasmine expectation: content_copy const component = fixture. componentInstance; expect(component).

Which TestBed method is used to create an Angular component under test?

Which of the following TestBed method is used to create an Angular component under test? The correct answer is - createComponent!


Video Answer


1 Answers

Testing AngularJS components doesn't differ much from testing directives.

To test controller's methods / properties, you can access the instance of the component's controller using element.controller("componentName") method (componentName - is a camelCase directive / component name).

Here is example using $compile service to test the component and $onChanges hook:

angular.module('myApp', [])
.component('myComponent', {
    bindings: {
        myBinding: '<'
    },
    template: '<div>{{$ctrl.result}}</div>',
    controller: 'myComponentController'
})
.controller('myComponentController', ['$filter', 'myService', function myComponentController($filter, myService) {
    var ctrl = this;

    ctrl.$onInit = onInit;
    ctrl.$onChanges = onChanges;

    function onInit() {
        ctrl.result = ctrl.myBinding;
    }

    function onChanges(changes) {
        if (angular.isDefined(changes.myBinding)) {
            if (angular.isDefined(changes.myBinding.currentValue)) {
                if (!angular.equals(changes.myBinding.currentValue, changes.myBinding.previousValue)) {
                    myService.doSomething(changes.myBinding.currentValue).then(
                        function (data) {
                            ctrl.result = data; 
                        }
                    );
                }
            }
        }
    }
}])
.service('myService', ['$timeout', function ($timeout) {
    return {
        doSomething: function (x) {
            return $timeout(function () {
                return x * 3;
            }, 500);
        }
    };
}]);


/*
TEST GO HERE 
*/

describe('Testing a component controller', function() {
  var $scope, ctrl, $timeout, myService;
  
    beforeEach(module('myApp', function ($provide) {

    }));
  
    beforeEach(inject(function ($injector) {
        myService = $injector.get('myService');
        $timeout = $injector.get('$timeout');
    }));
  
    describe('with $compile', function () { 
        var element;
        var scope;
        var controller;
        
        beforeEach(inject(function ($rootScope, $compile) {
            scope = $rootScope.$new();
            scope.myBinding = 10;
            element = angular.element('<my-component my-binding="myBinding"></my-component>');
            element = $compile(element)(scope);
            controller = element.controller('myComponent');
            scope.$apply();
        }));
      
        
         it('should render template', function () {
           expect(element[0].innerText).toBe('10'); //initial
           $timeout.flush(); //onchanges happened and promise resolved from the service
           //undefined -> 10
           expect(element[0].innerText).toBe('30'); 
         });
         
         
         it('should reflect to changes', function () {
           spyOn(myService, "doSomething").and.callThrough();
           scope.myBinding = 15; //change the binding
           scope.$apply(); //we need to call $apply to pass the changes down to the component
           $timeout.flush();
           expect(myService.doSomething).toHaveBeenCalled(); // check if service method was called 
           expect(controller.result).toBe(45); // check controller's result value 
         });
         
    })

});
.as-console-wrapper {
  height:0;
}
<!DOCTYPE html>
<html>

  <head>
    <!-- jasmine -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine.js"></script>
    <!-- jasmine's html reporting code and css -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine-html.js"></script>
    <link href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine.css" rel="stylesheet" />
    
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/boot.js"></script>
    <!-- angular itself -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>
    <!-- angular's testing helpers -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-mocks.js"></script>
  </head>

  <body>
    <!-- bootstrap jasmine! -->
  <script>
    var jasmineEnv = jasmine.getEnv();
    
    // Tell it to add an Html Reporter
    // this will add detailed HTML-formatted results
    // for each spec ran.
    jasmineEnv.addReporter(new jasmine.HtmlReporter());
    
    // Execute the tests!
    jasmineEnv.execute();
  </script>
  </body>

</html>

You can also test your components using $componentController service. But in this case you will need to explicitly call life-cycle hooks in your tests, like:

ctrl = $componentController('myComponent', {$scope: scope}, { myBinding: 10 });
ctrl.$onInit();

To test $onChanges hook, you will need to pass a "properly" constructed changes object as argument:

angular.module('myApp', [])
    .component('myComponent', {
        bindings: {
            myBinding: '<'
        },
        template: '<div>{{$ctrl.result}}</div>',
        controller: 'myComponentController'
    })
    .controller('myComponentController', ['$filter', 'myService', function myComponentController($filter, myService) {
        var ctrl = this;

        ctrl.$onInit = onInit;
        ctrl.$onChanges = onChanges;

        function onInit() {
            ctrl.result = ctrl.myBinding;
        }

        function onChanges(changes) {
            if (angular.isDefined(changes.myBinding)) {
                if (angular.isDefined(changes.myBinding.currentValue)) {
                    if (!angular.equals(changes.myBinding.currentValue, changes.myBinding.previousValue)) {
                        myService.doSomething(changes.myBinding.currentValue).then(
                            function (data) {
                                ctrl.result = data;
                            }
                        );
                    }
                }
            }
        }
    }])
    .service('myService', ['$timeout', function ($timeout) {
        return {
            doSomething: function (x) {
                return $timeout(function () {
                    return x * 3;
                }, 500);
            }
        };
    }]);


/*
TEST GO HERE 
*/

describe('Testing a component controller', function () {
    var $scope, ctrl, $timeout, myService;

    beforeEach(module('myApp', function ($provide) {

    }));

    beforeEach(inject(function ($injector) {
        myService = $injector.get('myService');
        $timeout = $injector.get('$timeout');
    }));

    describe('with $componentController', function () {
        var scope;
        var controller;

        beforeEach(inject(function ($rootScope, $componentController) {
            scope = $rootScope.$new();
            scope.myBinding = 10;

            controller = $componentController('myComponent', {$scope: scope}, {myBinding: 10});
            controller.$onInit();
        }));

        it('should reflect to changes', function () {
            spyOn(myService, "doSomething").and.callThrough();
            controller.$onChanges({myBinding: {currentValue: 15, previousValue: 10}});
            $timeout.flush(); // resolve service promise 
            expect(myService.doSomething).toHaveBeenCalled(); // check if service method was called 
            expect(controller.result).toBe(45); // check controller's result value 
        });

    })

});
.as-console-wrapper {
  height:0;
}
<!DOCTYPE html>
<html>

  <head>
    <!-- jasmine -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine.js"></script>
    <!-- jasmine's html reporting code and css -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine-html.js"></script>
    <link href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/jasmine.css" rel="stylesheet" />
    
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.8.0/boot.js"></script>
    <!-- angular itself -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>
    <!-- angular's testing helpers -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-mocks.js"></script>
  </head>

  <body>
    <!-- bootstrap jasmine! -->
  <script>
    var jasmineEnv = jasmine.getEnv();
    
    // Tell it to add an Html Reporter
    // this will add detailed HTML-formatted results
    // for each spec ran.
    jasmineEnv.addReporter(new jasmine.HtmlReporter());
    
    // Execute the tests!
    jasmineEnv.execute();
  </script>
  </body>

</html>

P.S.: $onChange is not a valid name of the component's life-cycle hook. It should be $onChanges.

like image 91
Stanislav Kvitash Avatar answered Oct 20 '22 16:10

Stanislav Kvitash