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.