Jasmine

AngularJS, Pact, and Jasmine

  1. Follow the steps in pact-consumer-js-dsl (yes, with the gem ‘pact-mock_service’)
  2. Here is where the important stuff comes with AngularJS:

given service:

angular.module('ng-cherie.services.current-theme', [
    'ng-cherie.constants'
])
    .factory('currentThemeService', function currentThemeService($http, API) {
        var service = {
            getCurrentTheme: getCurrentTheme
        };

        function getCurrentTheme() {
            var theme;
            return $http.get(API.BASE_PATH + API.CURRENT_THEME)
                .then(function getCurrentThemeSuccess(response) {
                    theme = response.data;

                    return theme;
                }, function() {
                    return null;
                });
        }

        return service;
    });

then pact with ngMidwayTester

/* globals ngMidwayTester */ 
/* globals Pact */
describe('currentThemeService', function() {
    var API,
        currentThemeProvider,
        currentThemeService,
        mockCurrentTheme,
        $httpBackend,
        tester;

    beforeEach(function() {
        angular.module('currentThemeServiceSpec', [
          'ng-cherie.services.current-theme',
          'ng-cherie.constants',
          'ng-cherie.mocks.current-theme',
          'ngMockE2E'
        ]);

        tester = ngMidwayTester('currentThemeServiceSpec');
        currentThemeService = tester.inject('currentThemeService');
        API = tester.inject('API');
        mockCurrentTheme = tester.inject('mockCurrentTheme');

        $httpBackend = tester.inject('$httpBackend');
        $httpBackend.whenGET(/\/api\/v1\/currenttheme/).passThrough();

        currentThemeProvider = Pact.mockService({
            consumer: 'ng-cherie.services.current-theme',
            provider: 'CherieApp',
            port: 1234,
            done: function (error) {
                expect(error).toBe(null);
            }
        });
    });

    afterEach(function () {
        tester.destroy();
        tester = null;
    });

    describe('getCurrentTheme', function() {
       it('should return the current theme via pact', function(done) {

           API.BASE_PATH = "http://localhost:1234" + API.BASE_PATH;

           currentThemeProvider
               .uponReceiving("a request for the current theme")
               .withRequest("get", "/api/v1/currenttheme")
               .willRespondWith(200, { "Content-Type": "application/json" }, mockCurrentTheme);

           currentThemeProvider.run(done, function(runComplete) {
               currentThemeService.getCurrentTheme().then(function(response) {
                   expect(response).toEqual(mockCurrentTheme);
                   runComplete();
               });
           });
       });
    });

});

line 12: the key is to set up a new module that contains ngMockE2E so we can use `passthrough`
line 45: make sure to replace the domain with the pact server location, in this case, it is localhost:1234.

Advertisements

AngularJS compile vs templateUrl

Hello, hello, it has been a while since my last post of ‘given, then, show’. If you are new to here, this is not a recommendation on how to implement, but rather just a show and tell of what you can do.

Given:

– Implement a directive that handles these providers:
– Provider A, B, C, and D share the same template
– Provider A and B have different description
– Provider C and D have no description
– Provider E and F have different templates and different description

Then:

– Assuming that templates and provider descriptions are dumb aka not stored in controllers
– Implement a directive that shows the default template, which is share across the similar providers (A, B, C, D) and a separate template per unique provider E and F.
– Implement a directive just for passing in unique descriptions

————————————>>>>>>————————————
Call the directive

<demo provider="provider"></demo>

Method 1a: use compiler


angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                $compile(elem.html(template).contents())(scope);
            },
            scope: {
                provider: '='
            }
        };
    });

Method 1b: use compiler

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                elem.append($compile(template)(scope));
            },
            scope: {
                provider: '='
            }
        };
    });

Method 2: use ng-include

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective() {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            scope: {
                provider: '='
            },
            templateUrl: 'app/directives/demo/templates/demo.tpl.html'
        };
    });

where app/directives/demo/templates/demo.tpl.html is

<ng-include src="demoCtrl.showTemplate()"></ng-include>

where showTemplate() determines which template to show depending on the provider (similar to demoLink())

————————————>>>>>>————————————
in default.tpl.html:
use a directive (demoDescription directive) to pass in the description and a control to show the description, so that when demo directive (see above) renders the default template directly without the description, it won’t fail on missing transclusion.

<p class="demo-description" ng-transclude ng-if="demoDescriptionCtrl.isTemplateUsedWithDirective"></p>

call the directive:

<demo-description>
    {{ 'Provider description here.' | translate }}
</demo-description>

————————————>>>>>>————————————
jasmine test

describe('Demo directive', function() {
    var _,
        $compile,
        element,
        providerMockData,
        scope;

    beforeEach(module('ng-cherie.directives.demo'));
    beforeEach(module('ng-cherie.mocks.provider'));

    beforeEach(inject(function($injector) {
        _ = $injector.get('_');
        $compile = $injector.get('$compile');
        scope = $injector.get('$rootScope').$new();
        providerMockData = $injector.get('providerMockData');
    }));

    beforeEach(function() {
        scope.provider = _.first(providerMockData.providers);
    });

    function compileDirective(scope) {
        var compiledElement = $compile('<demo provider="provider"></demo>')(scope);

        scope.$digest();

        return compiledElement;
    }

    describe('compiled markup', function() {
        it('contains the expected markup', function() {
            element = compileDirective(scope);

            expect(element[0].querySelector('.demo-body')).not.toBeNull();
        });

        //some other tests here... mostly expecting certain classes to be or not be there

        it('shows the default template when the provider is unknown', function () {
            scope.provider.id = "unexpected_provider";
            element = compileDirective(scope);

            expect(element.find('demo-description').length).toBe(0);
        });
    });
});

rails, getJSON, and jasmine

Task:
When user selects a country from the country dropdown,
get the new terms and conditions links according to the selected country,
and update the current terms and conditions links with the new ones.

controller

def update_policy_links
    respond_to do |format|
      format.js {
        render :json => {:updated_terms_link => 
                          return_policy_link("terms", country_preference),
                         :updated_privacy_link => 
                          return_policy_link("privacy", country_preference),
                         :updated_electronic_link => 
                          return_policy_link("electronic", country_preference)}
      }
    end
end

country_preference: a cookie that stores the latest country user selected
return_policy_link: check out the previous post on default value & nil guard

routes

get '/update_policy_links/:country' => "enrollment/user#update_policy_links", :constraints => {:country => /[a-zA-Z]{2}/}

note: we’re passing in country as a param for other usage which I won’t go over here…

javascript

$("#new_vuser").on('blur', '#user_countryCode', function(){
    country_selected = $(this).val();

    $.getJSON("/update_policy_links/" + country_selected + "?locale=" + I18n.locale, function(data){
          $(".terms_link, #terms_link").attr('href', data.updated_terms_link);
          $(".privacy_link, #privacy_link").attr('href', data.updated_privacy_link);
          $(".electronic_link").attr('href', data.updated_electronic_link);
    });
});

jasmine

describe("on_select_change", function(){
    it("should update the terms and condition links", function(){

        var links ={
            updated_terms_link : "/pages/terms",
            updated_privacy_link : "/pages/privacy",
            updated_electronic_link : "/pages/electronic"
        };

        spyOn($, 'getJSON').andCallFake(function(url,data){ data(links); });
        $('#user_countryCode').blur();

        expect($.getJSON).wasCalled();
        expect($('.terms_link, #terms_link')).toHaveAttr('href',"/pages/terms");
        expect($('.privacy_link, #privacy_link')).toHaveAttr('href',"/pages/privacy");
        expect($('.electronic_link')).toHaveAttr('href',"/pages/electronic");
    });
})

Jasmine – how to mock window.location

I got some javascript code that I wanna test using Jasmine:

Cherie.Controllers.Help = function () {
    var $question = $('.question'),
        $help_section = $('#help_section');

    var event_handlers = {
        hide_event:{
            highlight_questions:function () {
                if (window.location.pathname == "/help") {
                    $question.each(function () {
                        if ($(this).attr('class').indexOf('highlight') == -1)
                            $(this).hide();
                    })
                }

            }
        },

        click_event:{
            question_list:function () {
                $help_section.on('click', '.question', function () {
                    $(this).next('.answer').toggle();
                    $this.find('.toggle').toggleClass('hide');
                });
            }
        }
    }

    $.each(event_handlers, function(i,handler_type){ $.each(handler_type, function(i,handler){ handler(); }) });
}

$(function() {
    Cherie.Controllers.Help();

});

I am gonna start with testing the hide_event, so I got to mock window.location.pathname… window.location can be written without the window prefix, and location is an object, which makes pathname a property (location.property), so we can’t “spyon” it, since spyon is used for mocking/stubbing functions, so let’s work around it so that we can use spyOn.

make a helper function:

var get_current_location = {
    get_pathname: function () {
        return window.location.pathname;
    }
};

and now the new code looks like this:

        hide_event:{
            highlight_questions:function () {
                if (get_current_location.get_pathname() == "/help") {
                    $question.each(function () {
                        if ($(this).attr('class').indexOf('highlight') == -1)
                            $(this).hide();
                    })
                }

            }
        }

great, on to jasmine…

describe("help.js", function () {
    beforeEach(function () {
        loadFixtures('help.html');
    });

    describe("hide_event", function (){
        it("should hide non-highlighted questions", function () {
            spyOn(get_current_location, 'get_pathname').andReturn("/help");
            Cherie.Controllers.Help();
            expect($('.highlight')).toBeVisible();
            expect($('.question:not(.highlight)')).toBeHidden();
        });

    });

    describe("click_event", function(){
        beforeEach(function(){
            Cherie.Controllers.Help();
            $('.question').first().click();
        });

        it("should show the corresponding answer", function(){
            expect($('.answer').first()).toBeVisible();
        });

        it("should show a '-' image with .hide", function(){
            expect($('.toggle').first()).toHaveClass('hide');
        });

        it("should show a '+' image without .hide on 2nd click", function(){
            $('.question').first().click();
            expect($('.toggle').first()).not.toHaveClass('hide');
        })
    });


});

& it’s alllll good (:

sidenote: The reason we have it as javascript is because we use the same partial across several different pages (not all questions are hidden); another way to implement it w/o javascript is to use conditionals along with css such as .hidden{display:none;}

hide_question = request.path == "/help" ? "hidden" : "" 

and then add the following to certain question list items.

{:class => "#{hide_question}"} 

There are a couple different ways to do this but you get the gist (; which one do you like better? javascript or rails way?