Category Archives: JavaScript

IntelliJ : Karma Integration

Overview

Although we can run karma start karma.conf.js on the command line to start Karma test runner, JetBrains provides a great Karma-IntelliJ integration to run and display the JavaScript test results within IntelliJ.

Prerequisites

Install Karma Plugin in IntelliJ

The first step is to install the Karma plugin created by JetBrains.

Create Karma “Run” Configuration

From the drop down list on top right of IntelliJ, select Edit Configurations….

In Run/Debug Configuration dialog, select + and then select Karma.

Enter a name, in this example, we call it Karma.

Use the drop down list to select a configuration file called karma.conf.js.

The Node interpreter and Karma package should already be defined, otherwise, use the drop down lists to select the right values.

On top right of IntelliJ, select the green Play button to run Karma test runner.

IntelliJ should now run Karma and display the familiar green/red bar based on the JavaScript test results.

There are few things we can configure here:-

  • Button 1 – When selected, IntelliJ will automatically rerun all the tests whenever we change the production or test JavaScript files.
  • Button 2 – When unselected, all passed tests are displayed.
  • Button 3 – When selected, all tests are expanded.

Advertisements

Karma: Getting Started

Overview

This tutorial walks through the steps need to configure Karma to work with a Maven web project. It will also be used as a base for my future Karma related posts.

Install Node.js

First, download Node.js from http://nodejs.org/download/ and install it. Once installed, we should be able to invoke npm command from the command line, for example:-

npm -version

Create package.json

Let’s assume we have the following Maven project:-

testKarma
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   ├── resources
│   │   └── webapp
│   └── test
│       └── java
└── testKarma.iml

Please ignore testKarma.iml, which is an IntelliJ specific file.

Create a file called package.json under project root directory…

testKarma
├── package.json
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   ├── resources
│   │   └── webapp
│   └── test
│       └── java
└── testKarma.iml

… with the following content…

{
  "name": "testKarma",
  "private": true
}

In this case, we created a private repository by setting private to true.

Without this flag set, when you run npm install ..., we will get these annoying warnings:-

npm WARN package.json testKarma@ No description
npm WARN package.json testKarma@ No repository field.
npm WARN package.json testKarma@ No README data

Install Karma and Plugins

From the project root directory, run the following command:-

npm install karma karma-jasmine@0.2.2 karma-chrome-launcher karma-phantomjs-launcher karma-junit-reporter karma-coverage --save-dev

At the time of this post, by default, karma-jasmine will install version 0.1.5. So, we will manually specify the latest version that allows us to use Jasmine 2.x.

For local testing, we will run our tests against both Chrome browser and PhantomJS, which is a headless browser. So, make sure Chrome browser is already installed.

The project structure now contains the installed plugins for JavaScript testing:-

testKarma
├── node_modules
│   ├── karma
│   ├── karma-chrome-launcher
│   ├── karma-coverage
│   ├── karma-jasmine
│   ├── karma-junit-reporter
│   └── karma-phantomjs-launcher
├── package.json
├── pom.xml
├── src
│   ├── main
│   └── test
└── testKarma.iml

The package.json contains a history of the plugins we installed within this project:-

{
  "name": "testKarma",
  "private": true,
  "devDependencies": {
    "karma": "^0.12.24",
    "karma-chrome-launcher": "^0.1.5",
    "karma-coverage": "^0.2.6",
    "karma-jasmine": "^0.2.2",
    "karma-junit-reporter": "^0.2.2",
    "karma-phantomjs-launcher": "^0.1.4"
  }
}

A couple of important notes:-

  • Basically, since we are not running npm install with -g option, the plugins will not be installed globally. Rather, they are installed within the project root directory.
  • Using --save-dev option, if package.json exists, this file will be automatically updated to keep track of the installed plugins and versions.
  • If we decided to update the plugin versions, we just need to modify this file and run npm install to update them.
  • We do not want to commit node_modules directory into any VCS because there are at least 5000 files in this directory. So, remember to configure a VCS exclusion on this directory (see IntelliJ: Handling SVN Global Ignore List).
  • When other peers check out this project from VCS, they will run npm install, which will automatically install all the dependencies listed in package.json.

Create Karma Configuration File

Instead of running karma init karma.conf.js to step through the process to create a configuration file, we will manually create two Karma configuration files under src/test/resources directory.

testKarma
├── node_modules
├── package.json
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   ├── resources
│   │   └── webapp
│   └── test
│       ├── java
│       └── resources
│           ├── karma.conf.js
│           └── karma.jenkins.conf.js

karma.conf.js

This configuration is used for local testing.

module.exports = function ( config ) {
    config.set( {
        basePath         : '../../../',
        frameworks       : ['jasmine'],
        files            : [
            'src/main/webapp/resources/js/**/*.js',
            'src/test/js/**/*.js'
        ],
        exclude          : [],
        preprocessors    : {
            'src/main/webapp/resources/js/**/*.js' : ['coverage']
        },
        reporters        : ['progress', 'coverage'],
        port             : 9876,
        colors           : true,
        logLevel         : config.LOG_INFO,
        autoWatch        : true,
        browsers         : ['Chrome', 'PhantomJS'],
        singleRun        : false,
        plugins          : [
            'karma-jasmine',
            'karma-chrome-launcher',
            'karma-phantomjs-launcher',
            'karma-junit-reporter',
            'karma-coverage'
        ],
        coverageReporter : {
            type : 'html',
            dir  : 'target/coverage/'
        }
    } );
};

karma.jenkins.conf.js

This configuration is used for automated testing on Jenkins.

module.exports = function ( config ) {
    config.set( {
        basePath         : '../../../',
        frameworks       : ['jasmine'],
        files            : [
            'src/main/webapp/resources/js/**/*.js',
            'src/test/js/**/*.js'
        ],
        exclude          : [],
        preprocessors    : {
            'src/main/webapp/resources/js/**/*.js' : ['coverage']
        },
        // added `junit`
        reporters        : ['progress', 'junit', 'coverage'],
        port             : 9876,
        colors           : true,
        logLevel         : config.LOG_INFO,
        // don't watch for file change
        autoWatch        : false,
        // only runs on headless browser
        browsers         : ['PhantomJS'],
        // just run one time
        singleRun        : true,
        // remove `karma-chrome-launcher` because we will be running on headless
        // browser on Jenkins
        plugins          : [
            'karma-jasmine',
            'karma-phantomjs-launcher',
            'karma-junit-reporter',
            'karma-coverage'
        ],
        // changes type to `cobertura`
        coverageReporter : {
            type : 'cobertura',
            dir  : 'target/coverage-reports/'
        },
        // saves report at `target/surefire-reports/TEST-*.xml` because Jenkins
        // looks for this location and file prefix by default.
        junitReporter    : {
            outputFile : 'target/surefire-reports/TEST-karma-results.xml'
        }
    } );
};

Write JS Production Code and Test Code

Now, it’s time to write some tests and run them.

testKarma
├── node_modules
├── package.json
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   ├── resources
│   │   └── webapp
│   │       └── resources
│   │           └── js
│   │               └── hello.js
│   └── test
│       ├── java
│       ├── js
│       │   └── hello-spec.js
│       └── resources
│           ├── karma.conf.js
│           └── karma.jenkins.conf.js
└── testKarma.iml

hello.js

We will keep our production code to the minimum in this example:-

var hello = {
    speak : function () {
        return 'Hello!';
    }
};

hello-spec.js

A very simple test case:-

describe( 'hello module', function () {
	'use strict';
    it( 'speak()', function () {
        expect( hello.speak() ).toBe( 'Hello!' );
    } );
} );

Run Karma using karma.conf.js

From the project root directory, run Karma:-

node_modules/karma/bin/karma start src/test/resources/karma.conf.js

The console should now look like this:-

INFO [karma]: Karma v0.12.24 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket YNMziWTsyeaf6DZzw06D with id 3320523
INFO [Chrome 38.0.2125 (Mac OS X 10.9.5)]: Connected on socket rYQeni1xm1bbqfa3w06E with id 50259203
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 SUCCESS (0.003 secs / 0.001 secs)
Chrome 38.0.2125 (Mac OS X 10.9.5): Executed 1 of 1 SUCCESS (0.008 secs / 0.001 secs)
TOTAL: 2 SUCCESS

The target/coverage directory should contain Chrome and PhantomJS subdirectories.

testKarma
├── node_modules
├── package.json
├── pom.xml
├── src
├── target
│   └── coverage
│       ├── Chrome 38.0.2125 (Mac OS X 10.9.5)
│       │   ├── index.html
│       │   ├── js
│       │   │   ├── hello.js.html
│       │   │   └── index.html
│       │   ├── prettify.css
│       │   └── prettify.js
│       └── PhantomJS 1.9.8 (Mac OS X)
│           ├── index.html
│           ├── js
│           │   ├── hello.js.html
│           │   └── index.html
│           ├── prettify.css
│           └── prettify.js
└── testKarma.iml

When opening one of the index.html files, we should see the coverage report.

Run Karma using karma.jenkins.conf.js

Although we will only use karma.jenkins.conf.js for automated testing on Jenkins, we will run this configuration file to see the generated output differences.

Clean the target directory:-

mvn clean

Run Karma with karma.jenkins.conf.js instead:-

node_modules/karma/bin/karma start src/test/resources/karma.jenkins.conf.js

Since this configuration only runs on the headless browser, we will not see Chrome results here:-

INFO [karma]: Karma v0.12.24 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket SM_5sy2wzHdL4ru9yg0X with id 42922522
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 SUCCESS (0.002 secs / 0.001 secs)

The target directory should contain cobertura-coverage.xml for coverage result and TEST-karma-results.xml for test result.

testKarma
├── node_modules
├── package.json
├── pom.xml
├── src
├── target
│   ├── coverage-reports
│   │   └── PhantomJS 1.9.8 (Mac OS X)
│   │       └── cobertura-coverage.xml
│   └── surefire-reports
│       └── TEST-karma-results.xml
└── testKarma.iml

Combining and Minifying JavaScript Files with Google Closure Compiler

GOAL

The goal is to combine and minify several JS files into one JS file in the right order.

PROBLEM

Let’s assume we have the following directory structure with three JS files.

Directory Structure

appdev
└── src
    ├── appdev.blackcow.js
    ├── appdev.js
    └── appdev.whitesheep.js

appdev.js

var AppDev = {
    modules : [],

    start : function () {
        var moduleName;
        for ( moduleName in AppDev.modules ) {
            if ( AppDev.modules.hasOwnProperty( moduleName ) ) {
                AppDev.modules[moduleName]();
            }
        }
    }
};

appdev.blackcow.js

AppDev.modules.blackcow = function ( config ) {
    console.log( 'in black cow module...', config );
};

appdev.whitesheep.js

AppDev.modules.whitesheep = function ( config ) {
    console.log( 'in white sheep module...', config );
};

SOLUTION

To pull this off, we will leverage Google Closure Compiler.

There are multiple ways to use this compiler, but if you are using Mac, the easiest approach, in my opinion, is to install this compiler using Brew.

brew install closure-compiler

Once installed, navigate to the appdev directory and run the following command:-

closure-compiler --js `find src/**/*.js` --js_output_file appdev.min.js

Now, a new file called appdev.min.js will be created.

appdev
├── appdev.min.js
└── src
    ├── appdev.blackcow.js
    ├── appdev.js
    └── appdev.whitesheep.js

The reformatted file content looks like this:-

AppDev.modules.blackcow = function ( a ) {
    console.log( "in black cow module...", a )
};
var AppDev = {
    modules : [], start : function () {
        for ( var a in AppDev.modules ) {
            if ( AppDev.modules.hasOwnProperty( a ) ) {
                AppDev.modules[a]()
            }
        }
    }
};
AppDev.modules.whitesheep = function ( a ) {
    console.log( "in white sheep module...", a )
};

The generated code is going to cause problem because the Closure Compiler basically appends each file content based on the order of the JS file names.

To fix this, we have to specify the dependencies so that the Closure Compiler will auto-sort the files correctly.

appdev.js

goog.provide('AppDev');

var AppDev = {
    modules : [],

    start : function () {
        var moduleName;
        for ( moduleName in AppDev.modules ) {
            if ( AppDev.modules.hasOwnProperty( moduleName ) ) {
                AppDev.modules[moduleName]();
            }
        }
    }
};

appdev.blackcow.js

goog.require('AppDev');

AppDev.modules.blackcow = function ( config ) {
    console.log( 'in black cow module...', config );
};

appdev.whitesheep.js

goog.require('AppDev');

AppDev.modules.whitesheep = function ( config ) {
    console.log( 'in white sheep module...', config );
};

After rerunning the compiler, the file content for appdev.min.js now looks correct.

var AppDev = {
    modules : [], start : function () {
        for ( var a in AppDev.modules ) {
            if ( AppDev.modules.hasOwnProperty( a ) ) {
                AppDev.modules[a]()
            }
        }
    }
};
AppDev.modules.blackcow = function ( a ) {
    console.log( "in black cow module...", a )
};
AppDev.modules.whitesheep = function ( a ) {
    console.log( "in white sheep module...", a )
};

Modernizr: Detecting Touch Devices vs Non Touch Devices

Modernizr has a great feature that allows us to detect whether the browser is running on a touch device (iPad, smartphones, etc) or on a non-touch device (laptop, computer, etc).

Even though most modern touch devices support “proximity” hovering (navigating without touching the device screen), the browsing experience still suffer when dealing with onmouseover and onmouseout events.

So, let’s assume we want to implement WordPress’ Posts table. When we hover over any table row on a non-touch device, it will show additional actions such as “Edit”, “Quick Edit”, “Trash” and “View”. When we view the same page on a touch device, it will show the action without the hovering option.

The table may look something like this:-

<table class="table-actionable">
    <tr>
        <th>Artist</th>
        <th>Song Title</th>
    </tr>
    <tr>
        <td>System Of a Down</td>
        <td>Toxicity
            <ul class="hidden-action hide">
                <li><a href="/epic-app/buy/1">Buy</a></li>
                <li><a href="/epic-app/sell/1">Sell</a></li>
                <li><a href="/epic-app/trash/1">Trash</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td>Eminem</td>
        <td>Rap God
            <ul class="hidden-action hide">
                <li><a href="/epic-app/buy/2">Buy</a></li>
                <li><a href="/epic-app/sell/2">Sell</a></li>
                <li><a href="/epic-app/trash/2">Trash</a></li>
            </ul>
        </td>
    </tr>
</table>

With Modernizr, we can now write something like this:-

<script>
    // On non-touch devices, show action list on mouse over and hide it on mouse out.
    if ( !Modernizr.touch ) {
        $( '.table-actionable tr' )
                .mouseover( function () {
                                $( this ).find( '.hidden-action' ).removeClass( 'hide' );
                            } )
                .mouseout( function () {
                               $( this ).find( '.hidden-action' ).addClass( 'hide' );
                           } );
    }
    // On touch devices, always show action list.
    else {
        $( '.hidden-action' ).removeClass( 'hide' );
    }
</script>

Shitty epic…

Backbone: model.destroy() Always Call “error” Callback Function

PROBLEM

Let’s assume we invoke person.destroy() to delete a person:-

var person = Backbone.Model.extend( { ... } );

person.destroy( {
    contentType : 'application/json',

    success : function () {
        console.log('success');
    },

    error : function () {
        console.log('error');
    }
} );

… and let’s assume when person.destroy() is invoked, it will call /person/{personId} DELETE. Here’s how the Spring MVC controller API might look like:-

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity(HttpStatus.OK);
    }
}

When we execute person.destroy(), the error callback function will always get called even though the HTTP status is a 200 OK.

Why?

SOLUTION

The reason is because Backbone expects a JSON payload from the web service call. In the example above, we return a 200 OK but without a JSON payload. Thus, Backbone will treat this as an error and invoke the error callback function instead of success callback function regardless of the HTTP status.

There are three solutions to this problem.

Solution 1: Add a JSON Payload

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity<Long>(personId, HttpStatus.OK);
    }
}

In this approach, we add a JSON payload to satisfy Backbone. In the above example, we return the value of personId back to the client. It can be an empty object too.

I don’t like this approach because that piece of information is useless to the client side. Even if I return an empty object, the solution seems very brittle because the next team member inheriting my code will not understand why there’s a need to return an empty object back to the client.

Solution 2: Accept Payload as Text

var person = Backbone.Model.extend( { ... } );

person.destroy( {
    contentType : 'application/json',
		
    dataType : 'text',
		
    success : function () {
        console.log('success');
    },

    error : function () {
        console.log('error');
    }
} );

Another approach is to accept the data as text from the server. This solution works fine if we are not expecting any complex data structure back from the server.

Solution 3: Return a Different HTTP Status

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

The approach that I truly recommend is to return a 204 No Content instead of 200 OK. This way, the client side will not expect any payload from the server, and thus, Backbone will invoke the success callback function.

Managing the Order of AJAX Calls on Input Field’s Keyup Event

SCENARIO

Consider the following code:-

$('#employeeSearchField').keyup(function() {
	var query = $(this).val();

	$.get('/employees/api/search', { q: query }, function(data) {
		// do stuff
		...
	});

}).trigger('keyup');

When user types an employee’s name, “Mike”, in the search field, a web service call is fired per character typed. In this example, the following web service calls are made:-

  • GET /employees/api/search?q=M
  • GET /employees/api/search?q=Mi
  • GET /employees/api/search?q=Mik
  • GET /employees/api/search?q=Mike

Let’s assume this web service searches the input string against databases (or flat files, Facebook API, etc) and returns a list of employee JSON objects where their names match the given input string. The code above will take the result and display the employee list on the view.

PROBLEM

Since we can’t control how long each web service call takes to process the request, the order of the returned JSON objects might not match the order of the web service calls. As a result, we may have stale information being presented on the view. Further, we may have the annoying “flicker” problem where the old employee list overrides the new employee list on the view.

SOLUTION

To ensure the order of the returned JSON objects matches the order of the web service calls, we need to keep track each AJAX call’s timestamp. In this example, I’m using Moment.js, a date library, but you can also use the built-in Date object. For now, think of Moment.js as a Swiss Army Knife for date, or a Rambo Knife for date, or a MacGyver Knife for date… okay, maybe not MacGyver Knife since MacGyver can use a tooth pick to solve the date problem.

// keep track latest AJAX call's timestamp
var latestAjaxCallDateTime;

$('#employeeSearchField').keyup(function() {
	var query = $(this).val();

	// before executing the AJAX call, store the latest timestamp
	latestAjaxCallDateTime = moment();
	
	// set the "soon-to-be-executed" AJAX call's timestamp to be the same 
	// as the latest timestamp
	var currentAjaxCallDateTime = latestAjaxCallDateTime;

	$.get('/employees/api/search', { q: query }, function(data) {

		// if current timestamp is older than the latest timestamp, then 
		// omit the request 
		if (currentAjaxCallDateTime.isBefore(latestAjaxCallDateTime)) {
			return;
		}

		// do stuff
		...
	});

}).trigger('keyup');

Yes, this looks pretty hacky, but it works. The whole idea is we will not process the result if it is old. The key of making this whole thing work is to set latestAjaxCallDateTime as a global variable and set currentAjaxCallDateTime as a global variable WITHIN the anonymous function of the input field’s keyup event.

Pretty Print JSON in JavaScript

PROBLEM

You want to display a JSON object in JavaScript.

TAKE 1

console.log(json);

While this works, I find this approach inconvenient when viewing the output in Firebug because I have to click on each generated link to view the details.

TAKE 2

console.log(JSON.stringify(json));

… will generate this:-

[{"title":"Holiday","id":"a1","start":"2014-02-03T09:00:00.000Z","allDay":true},{"title":"Pay Day","id":"a2","start":"2014-03-31T08:00:00.000Z","allDay":true}]

This approach will display the entire JSON object as one long string. This is better than me clicking on each generated link, but this is still fairly unreadable and cumbersome to me if I have a large JSON object.

TAKE 3

console.log(JSON.stringify(json, null, '\t'));

… will generate this:-

[
	{
		"title": "Holiday",
		"id": "a1",
		"start": "2014-02-03T09:00:00.000Z",
		"allDay": true
	},
	{
		"title": "Pay Day",
		"id": "a2",
		"start": "2014-03-31T08:00:00.000Z",
		"allDay": true
	}
]

Aw snap… a nicely formatted JSON string.