Tuesday, May 31, 2016

Testing Whether A Filter Was Called

Today I finally circled back on some old tests and decided to figure out how to tell whether a specific filter was called with the correct values.  This has been bothering me for a while, but the answer turned out to be pretty simple.  Let's say you have a controller that calls the built-in orderBy filter and passes an array of states (scope.states) to be sorted by id:
angular.module('app', []).controller('sampleController', function($scope, $filter) {
  $scope.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  $scope.sort = function() {
    return $filter('orderBy')(scope.states, 'id');
  };
});

The end result should be that these states get sorted so Alaska comes first, followed by Alabama, Arkansas, then Arizona (AK, AL, AR, AZ).  In order to verify this works as expected we can inject a new spy into the controller instead of using the expected $filter service.  Like this:
describe('test suite', function() {
  var scope, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(inject(function(_$controller_, _$rootScope_) {
    scope = _$rootScope_.$new();
    
    orderByFilterSpy = jasmine.createSpy();
    filterSpy = jasmine.createSpy().and.returnValue(orderByFilterSpy);
    
    _$controller_('sampleController', { $scope: scope, $filter: filterSpy });
  }));
  
  it('should pass scope.states and \'id\' to orderByFilter', function() {
    scope.sort();
    
    expect(filterSpy).toHaveBeenCalled();
    expect(orderByFilterSpy).toHaveBeenCalledWith([
      {name: 'Alabama', id: 'AL'},
      { name: 'Alaskas', id: 'AK' },
      { name: 'Arizona', id: 'AZ' },
      { name: 'Arkansas', id: 'AR' }], 'id');
  });
});

What we end up with is one passing test.  Remember to trust that the orderBy filter does what you're expecting.  That means you're not actually testing whether the result of calling scope.sort is a sorted array.  Instead you just check that you passed the right values to the right filter.  If you're calling a custom filter, make sure to test that filter thoroughly, but separately.

Tuesday, May 24, 2016

Testing $mdSidenav

I'm working with Angular Material on my current project and it's been interesting.  I'm writing unit tests to try to cover every line, branch, etc. (at least every reasonable combination) and I came across a situation where I needed to confirm that $mdSidenav was being called properly.  I'm using Jasmine and Karma and I couldn't get it to work.

Fortunately, I wasn't the only person who ran into this problem and I found part of my answer here.  That allowed me to verify that $mdSidenav() was called, but not what value was passed to it.  I had to add one more little piece and I was good to go.  Here's what I did.

My controller function:
$scope.openSidenav = function() {
    $mdSidenav('menu').toggle();
};

And my test:
it('openSidenav should pass \'menu\' to $mdSidenav.toggle', function() {
    $controller('toolbarController', { $scope: scope, hotkeys: hotkeys });
 
    scope.openSidenav();
 
    expect(sideNavToggleMock).toHaveBeenCalled();
    expect(passedSideNavId).toBe('menu');
});

But the most important part is in the setup of the tests. After I create the module (like this:
beforeEach(module('quotelite'));) I have to create a spy assigned to a global variable, then use the $provide service to register a factory with the name $mdSidenav and set it to... you know what? Here's the code:
beforeEach(module(function ($provide) {
    sideNavToggleMock = jasmine.createSpy('$mdSidenav');
    $provide.factory('$mdSidenav', function() {
        return function(sideNavId) {
            passedSideNavId = sideNavId;
            return {
                toggle: sideNavToggleMock
            };
        };
    });
}));

That allows me to check everything that needs to be checked.  I'll be honest when I say that I'm not 100% certain how that's working, but I know that it works and I'll figure out the "how" part later.  For completeness, here's my full spec:
describe('myController', function() {
    var $controller, scope, sideNavToggleMock, passedSideNavId;
 
    beforeEach(module('app'));

    beforeEach(module(function ($provide) {
        sideNavToggleMock = jasmine.createSpy('$mdSidenav');
        $provide.factory('$mdSidenav', function() {
            return function(sideNavId) {
                passedSideNavId = sideNavId;
                return {
                    toggle: sideNavToggleMock
                };
            };
        });
    }));
 
    beforeEach(inject(function(_$controller_, _$rootScope_){
        scope = _$rootScope_.$new();
        _$controller_('myController', { $scope: scope });
    }));
 
    it('openSidenav should pass \'menu\' to $mdSidenav.toggle', function() { 
        scope.openSidenav();
  
        expect(sideNavToggleMock).toHaveBeenCalled();
        expect(passedSideNavId).toBe('menu');
    });
});

Wednesday, May 18, 2016

File Upload with Angular

HTML5 offers some cool features for uploading files, including the ability to drag and drop a file from your computer onto an area of the web page and have that file uploaded.  But Angular doesn't come with a built-in way to do it.  Fortunately (and this is one of the reasons I love Angular so much) there's already a community-built, open-source directive available for just that.  Actually, there are a lot of them, but I picked one in particular and it's working pretty well so far.  The one I picked is called ng-file-upload and can be found here.

That's great and all, but it didn't quite get me all the way to where I needed to be.  For that I needed another post, found here.  In that post the author explains how to structure the request with a FormData object.  It boils down to this:
var fd = new FormData();
fd.append('file', file);
$http.post(uploadUrl, fd, {
    transformRequest: angular.identity,
    headers: {'Content-Type': undefined}
});
You can put that in the watch (though you should really abstract it into a service) on 'file' (or whatever your scope variable is that's bound to ng-model of ng-file-upload) and you'll be able to upload the file correctly.  I had to add a few other things, like usingangular.toJson(item)to stringify an object as part of the parameter to the server, but that's specific to my implementation. The above code should be enough to have a drag and drop feature that does what you need in a basic use case.

Tuesday, May 17, 2016

Using Batch Files to Make Things Easier

Right now I'm working purely on the front end of the project I'm on, so I'm not using an IDE for the most part.  Instead I'm running everything through npm, which makes significant use of the command prompt.  That means that every time I want to start debugging again I have to launch three separate command prompts (one for lite-server, one for gulp, and one for karma), issue a cd command to the correct directory (which is nested pretty deep), then issue my actual command (npm start, gulp watch, and npm test, respectively).  I got tired of doing that over and over so I wrote a batch file to do it for me.
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm start"
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && gulp watch"
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm test"
As an added bonus, I can use the /min switch to minimize the windows as soon as they're opened.
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm start"
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && gulp watch"
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm test"

Thursday, May 5, 2016

Smarter Spying in Jasmine

While I was testing some Angular code today I came across a section that was a bit difficult to test.  There's a try/catch block where the try calls a function (numeral.language) and then the catch calls the same block.  The issue with testing was that if I spied on numeral.language and used .and.throwError() the function would throw an error both times and my test would 'splode.  The workaround turned out to be pretty straightforward.


var callCount;
beforeEach(function() {
  callCount = 0;
  spyOn(numeral, 'language').and.callFake(function() {
    if (callCount === 0) {
      callCount++;
      throw new Error('Unknown language');
    }
  });
});

The first time through the method (when callCount is 0), manually throw an error (instead of using .and.throwError()) and increment callCount.  The next time the fake gets called, it won't throw an exception.  I'm still working on how to call through to the original method after that, but I don't need it right now and I didn't want to forget this.

Saturday, March 5, 2016

Legends of Angular

I have a knack for having to do things at work that are not as easy as they seem.  It's truly unbelievable how often I say "That should be very straightforward" and it turns into some sort of nightmare coding scenario.  Lucky for you (and me, since this is my repository for such information) I'm really good at what I do so I'm able to figure these things out.  Hence the title of the blog.  But I digress.  Well, not really because in order to digress I'd have to be on topic first and since I haven't gotten that far yet, I haven't digressed.  But now I have.

To the point!  We use a lot of <fieldset>s at work, which means we have a lot of <legend>s, too.  We had a (seemingly simple) task to change the text of a <legend> based on some other value.  No prob, Bob!  We'll just do this:
<fieldset>
    <legend>{{LegendText}}</legend>
</fieldset>
So that's what I did.  The end.  No, not really.

It turns out my {{LegendText}} wasn't updating like I thought it would.  Long story short, I had to write a directive to allow me to bind my value to my <legend>.  Yup, seriously.  It gets better, though.  In our specific instance we already had some code that overwrote the <legend> to make it the toggle control for the <fieldset> so I had to duplicate that inside my directive.  It ended up being pretty simple, so here it is:
angularApp.directive('legend', [
    function() {
        return {
            restrict: 'E', // only activate on element attribute
            require: '?ngModel', // get a hold of NgModelController
            link: function(scope, element, attrs, ngModel) {
                if (!ngModel) return; // do nothing if no ng-model

                // Specify how UI should be updated
                ngModel.$render = function() {
                    $(element).html("<span class='icon icon-select-arrow-dn'></span> " + ngModel.$viewValue).css("cursor", "pointer");
                    $(element).off("click");
                    $(element).on("click", function () {
                        toggleFieldsetContent($(element));
                    });
                };
            }
        };
    }
]);
You'll just have to accept that toggleFieldsetContent is defined somewhere else and I'm not going to show it to you.

Also, you should note that this isn't necessarily the best way to do this.  It's a way to do it and it worked for me in a pinch.  I may come back to it and clean it up one day and I may not remember to update this blog post when I do.  So there.

Tuesday, March 1, 2016

Unit Testing Plain Old JavaScript (Part 4)

We've already covered how to write unit tests (using test-driven development) in Jasmine.  We used it to create the purely HTML5/JavaScript (without jQuery) Hangman game.  For my next trick I'll show you how to use blanket.js to get code (line) coverage.  Note: unfortunately I can't find any simple code coverage tools that work in-browser to provide branch coverage so this is the best I have right now.

The first thing we need to do is download blanket.js.  Click the big "Download 1.2.2" (that's what it says as of this writing anyway) button on the middle of the page to view the minifed raw JS.  Copy/paste that into a file in your Hangman directory.  Oh, for reference my Hangman directory looks like this:

‐Hangman
‐‐lib
‐‐‐blanket
‐‐‐‐blanket.min.js
‐‐‐‐jasmine-blanket.js
‐‐‐jasmine
‐‐‐‐boot.js
‐‐‐‐console.js
‐‐‐‐jasmine.js
‐‐‐‐jasmine-html.js
‐‐spec
‐‐‐hangman.spec.js
‐‐src
‐‐‐hangman.js
‐‐styles
‐‐‐hangman.css
‐‐Hangman.html
‐‐jasmine.css
‐‐jasmine_favicon.png
‐‐SpecRunner.html

You can see I put blanket.min.js in a new folder called blanket in the lib folder.  This is just my preference, but it's important that no matter where you put the blanket file you know where it is so you can properly reference it later.  Important Note: blanket uses UTF-8 encoding so if you go the copy/paste route and use something like Notepad, it'll get screwed up and throw a weird error in your console.  I used Notepad++ and changed the encoding to UTF-8 and it was all fine after that.

OK, the next thing we need to do is download the blanket jasmine adapter.  You can see I saved mine in /lib/blanket/jasmine-blanket.js.  Again, this is just my preference, but make sure you know where you put it.  But wait, we're not finished yet.  We actually have to modify the jasmine adapter to work with Jasmine 2+.  I found this code in a Stack Overflow answer here.  Just swap out lines 43-89 with the code below and save the adapter file.
BlanketReporter.prototype = {
        specStarted: function(spec) {
            blanket.onTestStart();
        },

        specDone: function(result) {
            var passed = result.status === "passed" ? 1 : 0;
            blanket.onTestDone(1,passed);
        },

        jasmineDone: function() {
            blanket.onTestsDone();
        },

        log: function(str) {
            var console = jasmine.getGlobal().console;

            if (console && console.log) {
                console.log(str);
            }
        }
    };

    // export public
    jasmine.BlanketReporter = BlanketReporter;

    //override existing jasmine execute
    var originalJasmineExecute = jasmine.getEnv().execute;
    jasmine.getEnv().execute = function(){ console.log("waiting for blanket..."); };


    blanket.beforeStartTestRunner({
        checkRequirejs:true,
        callback:function(){
            jasmine.getEnv().addReporter(new jasmine.BlanketReporter());
            jasmine.getEnv().execute = originalJasmineExecute;
            jasmine.getEnv().execute();
        }
    });

At this point our files are ready, but what do we do with them?  The next part really is the easiest part.  In your SpecRunner.html file add a reference to blanket and blanket adapter.  My reference (because of the location of my files) looks like this:
<script src="lib/blanket/blanket.min.js" data-cover-adapter="lib/blanket/jasmine-blanket.js" data-cover-only="[src/hangman.js]"></script>
The data-cover-only attribute of the script file tells blanket which files to cover.  If you have multiples you can do a comma-separated list.  Blanket also supports regular expressions so you could look for all files in the src directory that end with .js if you wanted.  Since I only have one file, this works for me.

The last step is to set this up to run as a website instead of from your local files.  I'm a Windows user so for me it was a simple matter of adding a website.  Simple for me, but if you haven't done it before it could be a bit complicated so I'll walk you through it.

First you have to have IIS Manger installed on your machine.  If you don't, that's an entirely separate process that will require a different post and admin rights on your machine.  Sorry.

I like to keep my websites in the same place.  Since Windows uses C:\Inetpub\wwwroot as the default location, that's where I put my Hangman folder (so it's C:\Inetpub\wwwroot\Hangman).

If you do have IIS Manager installed, open it up (I usually use Windows > inetmgr to launch).  In the left windowpane you should see your machine name; click to expand it.  Expand the Sites folder.  Right-click on the Sites folder and choose Add Web Site.  In the "Site name" textbox give your site a name (for simplicity I just called mine Hangman).  In the "Physical path" textbox type or choose the physical path of your folder (C:\Inetpub\wwwroot\Hangman).  You can change the port to something else if you want (I used 7654 because I was sure it was free).  Click OK.  Congratulations you created a new website!

The only thing remaining would be if you want your Hangman.html to be your default document.  If you do, click on your new website in the left windowpane and double-click Default Document in the main windowpane.  Add a new default document (Add link in the right windowpane) and type in Hangman.html.  Now if you navigate to http://localhost:7654 you should see your hangman game.  To check out your tests and their coverage, just go to http://localhost:7654/SpecRunner.html.

That's all for now.  The next task is going to be to make this a two-player game using websockets.  That will be interesting.