Integrating with jshint, adding destination controls, and additional minor fixes.

Change-Id: I72eb61fb9e9d7031ffb69d989ebf388cb3193783
diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000..dac66df
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1,3 @@
+ifc
+node_modules
+server-root
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..16f23d1
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,29 @@
+{
+  "camelcase": true,
+  "curly": true,
+  "eqeqeq": true,
+  "expr": true,
+  "forin": true,
+  "freeze": true,
+  "immed": true,
+  "indent": 2,
+  "latedef": "nofunc",
+  "maxlen": 80,
+  "newcap": true,
+  "noarg": true,
+  "nonbsp": true,
+  "nonew": true,
+  "quotmark": "single",
+  "sub": true,
+  "trailing": true,
+  "undef": true,
+  "unused": "vars",
+
+  "browser": true,
+  "devel": true,
+  "node": true,
+
+  "globals": {
+    "window": true
+  }
+}
diff --git a/Makefile b/Makefile
index ca463ca..90a9d76 100644
--- a/Makefile
+++ b/Makefile
@@ -42,8 +42,12 @@
 	@cp $< $@
 	@echo "Copying static file $<"
 
+.PHONY: lint
+lint: node_modules
+	@jshint .
+
 .PHONY: test
-test: $(tests)
+test: lint $(tests)
 
 .PHONY: $(tests)
 $(tests): test/%: test/%.js test/* mocks/* ifc node_modules $(js_files)
diff --git a/mocks/google-maps.js b/mocks/google-maps.js
index 2a25bc8..272aab7 100644
--- a/mocks/google-maps.js
+++ b/mocks/google-maps.js
@@ -1,5 +1,5 @@
-var $ = require('../src/util/jquery')
-var defineClass = require('../src/util/define-class')
+var $ = require('../src/util/jquery');
+var defineClass = require('../src/util/define-class');
 
 var ControlPosition = {
   TOP_LEFT: 'tl',
@@ -11,7 +11,7 @@
     this.$ = $('<div>');
     this.$.appendTo(parent);
   },
-  
+
   publics: {
     push: function(child) {
       this.$.append(child);
@@ -27,11 +27,11 @@
   },
   LatLng: function(){},
   ControlPosition: ControlPosition,
-  
+
   places: {
     SearchBox: function(){}
   },
-  
+
   event: {
     addListener: function(){}
   }
diff --git a/mocks/vanadium.js b/mocks/vanadium.js
index 815df95..f7a978f 100644
--- a/mocks/vanadium.js
+++ b/mocks/vanadium.js
@@ -3,16 +3,17 @@
 var MockRuntime = defineClass({
   publics: {
     on: function(event, handler) {
-      if (event == 'crash')
-        this.crash.add(handler);
+      if (event === 'crash') {
+        this.onCrash.add(handler);
+      }
     },
     fireCrash: function(err) {
-      this.crash(err);
+      this.onCrash(err);
     }
   },
-  
+
   events: {
-    crash: 'private'
+    onCrash: 'private'
   }
 });
 
@@ -20,18 +21,18 @@
   init: function(t) {
     this.t = t;
   },
-  
+
   publics: {
     init: function(config, callback) {
       this.t.ok(config, 'has config');
       this.callback = callback;
     },
-    
+
     finishInit: function(err, runtime) {
       this.callback(err, runtime);
     }
   },
-  
+
   statics: {
     vlog: {
       levels: {
diff --git a/package.json b/package.json
index 7ed8b4c..1c4c638 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
   "devDependencies": {
     "browserify": "^10.2.4",
     "jsdom": "^3.1.2",
+    "jshint": "^2.8.0",
     "node-static": "^0.7.6",
     "tape": "^4.0.0"
   },
diff --git a/src/components/destination.js b/src/components/destination.js
new file mode 100644
index 0000000..7d0e411
--- /dev/null
+++ b/src/components/destination.js
@@ -0,0 +1,48 @@
+var $ = require('../util/jquery');
+var defineClass = require('../util/define-class');
+
+var Destination = defineClass({
+  publics: {
+    setSearchBounds: function(bounds) {
+      this.searchBox.setBounds(bounds);
+    },
+
+    setPlaceholder: function(placeholder) {
+      this.$searchBox.attr('placeholder', placeholder);
+    }
+  },
+
+  events: [
+    /**
+     * @param places (array of places)
+     */
+    'onSearch'
+  ],
+
+  constants: ['$'],
+
+  init: function(maps, placeholder, initial) {
+    var destination = this;
+
+    var $searchBox = $('<input>')
+      .attr('type', 'text');
+    this.$searchBox = $searchBox;
+
+    this.setPlaceholder(placeholder);
+
+    if (initial) {
+      $searchBox.prop('value', initial);
+    }
+
+    this.$ = $('<div>').addClass('destination')
+      .append($searchBox);
+
+    this.searchBox = new maps.places.SearchBox($searchBox[0]);
+
+    maps.event.addListener(this.searchBox, 'places_changed', function() {
+      destination.onSearch(destination.searchBox.getPlaces());
+    });
+  }
+});
+
+module.exports = Destination;
\ No newline at end of file
diff --git a/src/components/destinations.js b/src/components/destinations.js
new file mode 100644
index 0000000..088e02c
--- /dev/null
+++ b/src/components/destinations.js
@@ -0,0 +1,76 @@
+var $ = require('../util/jquery');
+var defineClass = require('../util/define-class');
+
+var strings = require('../strings').currentLocale;
+
+var Destination = require('./destination');
+
+var Destinations = defineClass({
+  publics: {
+    append: function(destinationName) {
+      var placeholder;
+      switch (this.destinations.length) {
+        case 0:
+          placeholder = strings['Origin'];
+          break;
+        case 1:
+          placeholder = strings['Destination'];
+          break;
+        case 2:
+          this.destinations[1].setPlaceholder(strings.destination(1));
+          /* falls through */
+        default:
+          placeholder = strings.destination(this.destinations.length);
+      }
+
+      var destination = this.addDestination(placeholder, destinationName);
+      this.$.append(destination.$);
+      this.destinations.push(destination);
+    },
+
+    /**
+     * @param handler callback receiving a <code>Destination</code> instance
+     *  each time a <code>Destination</code> is added. On initial add, the
+     *  callback is called with all current <code>Destination</code>s.
+     */
+    addDestinationBindingHandler: function(handler) {
+      this.onDestinationAdded.add(handler);
+      $.each(this.destinations, function(i, destination) {
+        handler(destination);
+      });
+    }
+  },
+
+  privates: {
+    addDestination: function(placeholder, destinationName) {
+      var destination = new Destination(this.maps, placeholder,
+        destinationName);
+      this.onDestinationAdded(destination);
+      return destination;
+    }
+  },
+
+  events: {
+    /**
+     * @param destination Destination instance
+     */
+    onDestinationAdded: 'private'
+  },
+
+  constants: ['$'],
+
+  init: function(maps, initial) {
+    this.maps = maps;
+    this.$ = $('<form>').addClass('destinations');
+
+    this.destinations = [];
+
+    initial = initial || [];
+
+    for (var i = 0; i < Math.max(initial.length, 2); i++) {
+      this.append(initial[i]);
+    }
+  }
+});
+
+module.exports = Destinations;
\ No newline at end of file
diff --git a/src/components/maps.js b/src/components/maps.js
index 3e52c98..122a628 100644
--- a/src/components/maps.js
+++ b/src/components/maps.js
@@ -1,8 +1,7 @@
-var global = require('global');
 var $ = require('../util/jquery');
 var defineClass = require('../util/define-class');
 
-var strings = require('../strings')();
+var Destinations = require('./destinations');
 var Messages = require('./messages');
 
 var Widget = defineClass({
@@ -14,66 +13,145 @@
         marker.setMap(null);
       });
     },
-    
+
+    closeActiveInfoWindow: function() {
+      if (this.activeInfoWindow) {
+        this.activeInfoWindow.close();
+      }
+      this.activeInfoWindow = null;
+    },
+
     message: function(message) {
       this.messages.push(message);
     }
   },
-  
-  constants: ['$'],
-  
+
+  privates: {
+    destinationSelectionWindow: defineClass.innerClass({
+      privates: {
+        renderInfo: function() {
+          var $info = $('<div>').addClass('destination-info');
+
+          $info.append($('<div>')
+            .addClass('title')
+            .text(this.place.name));
+
+          return $info[0];
+        }
+      },
+
+      init: function(place, createMarker) {
+        var widget = this.outer;
+        var maps = widget.maps;
+        var map = widget.map;
+
+        this.place = place;
+
+        var infoWindow = new maps.InfoWindow({
+          content: this.renderInfo(),
+          position: place.geometry.location
+        });
+
+        var marker;
+        if (createMarker) {
+          marker = new maps.Marker({
+            map: map,
+            title: place.name,
+            position: place.geometry.location
+          });
+
+          maps.event.addListener(marker, 'click', function() {
+            widget.setActiveInfoWindow(infoWindow, marker);
+          });
+
+          widget.markers.push(marker);
+        } else {
+          widget.setActiveInfoWindow(infoWindow);
+        }
+      }
+    }),
+
+    setActiveInfoWindow: function(infoWindow, marker) {
+      this.closeActiveInfoWindow();
+      this.activeInfoWindow = infoWindow;
+      infoWindow.open(this.map, marker);
+    },
+
+    centerOnCurrentLocation: function() {
+      var maps = this.maps;
+      var map = this.map;
+
+      // https://developers.google.com/maps/documentation/javascript/examples/map-geolocation
+      if (global.navigator && global.navigator.geolocation) {
+        global.navigator.geolocation.getCurrentPosition(function(position) {
+          map.setCenter(new maps.LatLng(position.coords.latitude,
+            position.coords.longitude));
+        });
+      }
+    },
+
+    bindDestinationControl: function (destination) {
+      var widget = this;
+      var maps = this.maps;
+      var map = this.map;
+
+      maps.event.addListener(map, 'bounds_changed', function() {
+        destination.setSearchBounds(map.getBounds());
+      });
+
+      destination.onSearch.add(function(places) {
+        widget.clearMarkers();
+        widget.closeActiveInfoWindow();
+        var bounds = new maps.LatLngBounds();
+
+        if (places.length === 1) {
+          var place = places[0];
+          widget.destinationSelectionWindow(place, false);
+
+          map.setCenter(place.geometry.location);
+        } else if (places.length > 1) {
+          $.each(places, function(i, place) {
+            widget.destinationSelectionWindow(place, true);
+            bounds.extend(place.geometry.location);
+          });
+
+          map.fitBounds(bounds);
+        }
+      });
+    }
+  },
+
+  constants: ['$', 'maps'],
+
   // https://developers.google.com/maps/documentation/javascript/tutorial
   init: function(maps) {
-    maps = maps || global.google.maps;
-    var widget = this;
-    
+    this.maps = maps = maps || global.google.maps;
+
     this.$ = $('<div>').addClass('map-canvas');
-    
+
     this.markers = [];
+    this.route = {};
+
     this.messages = new Messages();
-    
+    this.destinations = new Destinations(maps);
+
     var config = {
       zoom: 11,
       center: new maps.LatLng(37.4184, -122.0880) //Googleplex
     };
-    
+
     var map = new maps.Map(this.$[0], config);
-  
-    // https://developers.google.com/maps/documentation/javascript/examples/map-geolocation
-    if (global.navigator && global.navigator.geolocation) {
-      global.navigator.geolocation.getCurrentPosition(function(position) {
-        map.setCenter(new maps.LatLng(position.coords.latitude,
-          position.coords.longitude));
-      });
-    }
-  
+    this.map = map;
+
+    this.centerOnCurrentLocation();
+
     var controls = map.controls;
-    
-    var $searchBox = $('<input>')
-      .attr('type', 'text')
-      .attr('placeholder', strings['Search']);
-    var txtSearchBox = $searchBox[0];
-    controls[maps.ControlPosition.TOP_LEFT].push(txtSearchBox);
-    
+
+    this.destinations.addDestinationBindingHandler(
+      $.proxy(this, 'bindDestinationControl'));
+
+    controls[maps.ControlPosition.TOP_LEFT].push(this.destinations.$[0]);
     controls[maps.ControlPosition.TOP_CENTER].push(this.messages.$[0]);
-    
-    var searchBox = new maps.places.SearchBox(txtSearchBox);
-    
-    maps.event.addListener(map, 'bounds_changed', function() {
-      searchBox.setBounds(map.getBounds());
-    });
-    
-    maps.event.addListener(searchBox, 'places_changed', function() {
-      var places = searchBox.getPlaces();
-      if (places.length == 1) {
-        var place = places[0];
-        widget.markers.push(new maps.Marker({
-          map: map,
-          title: place.name,
-          position: place.geometry.location
-        }));
-      }
-    });
   }
 });
 
diff --git a/src/components/message.js b/src/components/message.js
index a96e7cd..30248f9 100644
--- a/src/components/message.js
+++ b/src/components/message.js
@@ -7,49 +7,53 @@
 module.exports = {
   INFO: INFO,
   ERROR: ERROR,
-  
+
   info: function(text) {
     return {
       type: INFO,
       text: text
     };
   },
-  
+
   error: function(text) {
     return {
       type: ERROR,
       text: text
     };
   },
-  
+
   Message: defineClass({
     publics: {
       setType: function(type) {
-        if (type == INFO) {
-          this.$.attr('class', 'info');
-        } else if (type == ERROR) {
-          this.$.attr('class', 'error');
-        } else {
-          throw 'Invalid message type ' + type;
+        switch (type) {
+          case INFO:
+            this.$.attr('class', 'info');
+            break;
+          case ERROR:
+            this.$.attr('class', 'error');
+            break;
+          default:
+            throw 'Invalid message type ' + type;
         }
       },
-      
+
       setText: function(text) {
         this.$.text(text);
       },
-      
+
       set: function(message) {
         this.setType(message.type);
         this.setText(message.text);
       }
     },
-    
+
     constants: ['$'],
-    
+
     init: function(initial) {
       this.$ = $('<li>');
-      if (initial)
+      if (initial) {
         this.set(initial);
+      }
     }
   })
 };
diff --git a/src/components/messages.js b/src/components/messages.js
index 55eef83..f532df3 100644
--- a/src/components/messages.js
+++ b/src/components/messages.js
@@ -29,9 +29,9 @@
         });
     }
   },
-  
+
   constants: ['$'],
-  
+
   init: function() {
     this.$ = $('<ul>').addClass('messages');
   }
diff --git a/src/debug.js b/src/debug.js
index 97c15f5..26f3446 100644
--- a/src/debug.js
+++ b/src/debug.js
@@ -1,5 +1,3 @@
-var global = require('global');
-
 /**
  * Global variable exports for console debug.
  */
diff --git a/src/identity.js b/src/identity.js
index 762fdb4..27ba0da 100644
--- a/src/identity.js
+++ b/src/identity.js
@@ -1,5 +1,3 @@
-'use strict';
-
 var uuid = require('uuid');
 
 module.exports = Identity;
@@ -8,22 +6,24 @@
   this.username = extractUsername(accountName);
   this.deviceType = 'desktop';
   this.deviceId = uuid.v4();
-  
+
   this.deviceName = this.deviceType + '_' + this.deviceId;
   this.entityName = this.username + '/' + this.deviceName;
-};
+}
 
 function autoUsername() {
   return uuid.v4();
 }
 
 function extractUsername(accountName) {
-  if (!accountName || accountName === 'unknown')
+  if (!accountName || accountName === 'unknown') {
     return autoUsername();
-  
+  }
+
   var parts = accountName.split('/');
-  if (parts[0] !== 'dev.v.io' || parts[1] !== 'u')
+  if (parts[0] !== 'dev.v.io' || parts[1] !== 'u') {
     return accountName;
-  
+  }
+
   return parts[2];
 }
diff --git a/src/index.js b/src/index.js
index e304925..3efeeeb 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,8 +1,11 @@
 var $ = require('./util/jquery');
 var Travel = require('./travel');
 var debug = require('./debug');
+var strings = require('./strings').currentLocale;
 
 //http://api.jquery.com/ready/
 $(function() {
+  //http://stackoverflow.com/questions/180103/jquery-how-to-change-title-of-document-during-ready/11171548#11171548
+  $('html head title').text(strings['Travel Planner']);
   debug(new Travel());
 });
\ No newline at end of file
diff --git a/src/static/index.css b/src/static/index.css
index 358500e..e6f76af 100644
--- a/src/static/index.css
+++ b/src/static/index.css
@@ -3,6 +3,21 @@
   font-family: Arial, sans-serif;
 }
 
+.destinations {
+  margin: 2em 3em;
+  width: 30em;
+}
+
+.destination {
+  width: 100%;
+}
+
+.destination input {
+  width: 100%;
+  font-size: 15px;
+  padding: 8px 16px;
+}
+
 .map-canvas {
   width: 100%;
   height: 100%;
diff --git a/src/static/index.html b/src/static/index.html
index 2f50623..6d0be65 100644
--- a/src/static/index.html
+++ b/src/static/index.html
@@ -1,7 +1,7 @@
 <html>
   <head>
     <meta charset="utf-8">
-    <title>Google Travel</title>
+    <title>Travel Planner</title>
     <link rel="stylesheet" type="text/css" href="index.css">
     <script type="text/javascript"
       src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAQCvKWEWcSuQE2DSjVbvMKETSgF6S9i1k&signed_in=true&libraries=places">
diff --git a/src/strings.js b/src/strings.js
index 76bb6bb..d4e99fe 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -1,5 +1,14 @@
-module.exports = function() {
+function getStrings(locale) {
   return {
-    'Search': 'Search'
+    'Destination': 'Destination',
+    destination: function(n) {
+      return 'Destination ' + n;
+    },
+    'Origin': 'Origin',
+    'Travel Planner': 'Travel Planner'
   };
-};
\ No newline at end of file
+}
+
+getStrings.currentLocale = getStrings();
+
+module.exports = getStrings;
\ No newline at end of file
diff --git a/src/travel.js b/src/travel.js
index 7f4f8b9..d6f864b 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -9,36 +9,36 @@
 var TravelSync = require('./travelsync');
 var Identity = require('./identity');
 
-var strings = require('./strings')(/* TODO: locale */);
-
 var Travel = defineClass({
   publics: {
     error: function (err) {
       this.maps.message(message.error(err.toString()));
     },
-    
+
     info: function (info) {
       this.maps.message(message.info(info));
     }
   },
-  
+
   init: function (opts) {
     opts = opts || {};
     var vanadiumWrapper = opts.vanadiumWrapper || vanadiumWrapperDefault;
     var travel = this;
-    
+
     this.sync = new TravelSync();
-    
-    var reportError = $.proxy(this, 'error')
-    
+
+    var reportError = $.proxy(this, 'error');
+
     vanadiumWrapper.init(opts.vanadium).then(
       function(wrapper) {
+        wrapper.onCrash.add(reportError);
+
         var identity = new Identity(wrapper.getAccountName());
         identity.mountName = makeMountName(identity);
-        travel.sync.start(identity.mountName, wrapper).fail(reportError);
+        travel.sync.start(identity.mountName, wrapper).catch(reportError);
       }, reportError);
-    
-    this.maps = new Maps(opts.maps);    
+
+    this.maps = new Maps(opts.maps);
     var $domRoot = opts.domRoot? $(opts.domRoot) : $('body');
     $domRoot.append(travel.maps.$);
   }
diff --git a/src/travelsync.js b/src/travelsync.js
index 293c755..d2217b5 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -7,7 +7,7 @@
   init: function() {
     this.tripPlan = [];
     this.tripStatus = {};
-    
+
     // TODO: sync initial state
     this.server = new vdlTravel.TravelSync();
 
diff --git a/src/util/define-class.js b/src/util/define-class.js
index 2a48279..8df6c29 100644
--- a/src/util/define-class.js
+++ b/src/util/define-class.js
@@ -34,11 +34,13 @@
  * time. (As such, using that mechanism to declare private static constants does
  * work.)
  */
-module.exports = function defineClass(def) {
+module.exports = defineClass;
+
+function defineClass(def) {
   var constructor = function() {
     var pthis = $.extend({}, def.privates, def.publics, def.statics);
     var ifc = this;
-    
+
     if (def.events) {
       if ($.isArray(def.events)) {
         $.each(def.events, function(i, event) {
@@ -52,27 +54,44 @@
         defineEventsFromObject(pthis, ifc, def.events);
       }
     }
-    
-    if (def.statics)
+
+    if (def.statics) {
       $.extend(ifc, def.statics);
-    
-    if (def.init)
+    }
+
+    if (def.init) {
       def.init.apply(pthis, arguments);
-    
-    if (def.publics)
+    }
+
+    if (def.publics) {
       polyProxy(ifc, pthis, def.publics);
-    
+    }
+
     if (def.constants) {
       $.each(def.constants, function(i, constant) {
         ifc[constant] = pthis[constant];
       });
     }
   };
-  
-  if (def.statics)
+
+  if (def.statics) {
     $.extend(constructor, def.statics);
-  
+  }
+
   return constructor;
+}
+
+defineClass.innerClass = function(def) {
+  var init = def.init;
+  def.init = function(outer, constructorArgs) {
+    this.outer = outer;
+    init.apply(this, constructorArgs);
+  };
+
+  var InnerClass = defineClass(def);
+  return function() {
+    return new InnerClass(this, arguments);
+  };
 };
 
 function polyProxy(proxy, context, members) {
@@ -84,8 +103,9 @@
 
 function filterProxy(proxy, context, nameFilter) {
   $.each(context, function(name, member) {
-    if (nameFilter(name))
+    if (nameFilter(name)) {
       proxy[name] = $.proxy(member, context);
+    }
   });
   return proxy;
 }
@@ -95,15 +115,16 @@
   //Use polyProxy on function that fires to add the callable syntactic sugar
   var callableDispatcher = pthis[name] =
     polyProxy($.proxy(dispatcher, 'fire'), dispatcher, dispatcher);
-    
-  if (flags && flags.indexOf('private') > -1)
+
+  if (flags && flags.indexOf('private') > -1) {
     return;
-  
+  }
+
   if (flags && flags.indexOf('public') > -1) {
     ifc[name] = callableDispatcher;
   } else {
     ifc[name] = filterProxy({}, dispatcher, function(name) {
-      return name != 'fire' && name != 'fireWith';
+      return name !== 'fire' && name !== 'fireWith';
     });
   }
 }
diff --git a/src/vanadium-wrapper.js b/src/vanadium-wrapper.js
index 2774a7b..ceff029 100644
--- a/src/vanadium-wrapper.js
+++ b/src/vanadium-wrapper.js
@@ -6,75 +6,62 @@
 var VanadiumWrapper = defineClass({
   init: function(runtime) {
     this.runtime = runtime;
-    runtime.on('crash', this.crash);
+    runtime.on('crash', this.onCrash);
   },
-  
+
   publics: {
     getAccountName: function() {
       return this.runtime.accountName;
     },
-    
+
     /**
      * @param endpoint Vanadium name
      * @returns a promise resolving to a client or rejecting with an error.
      */
     client: function(endpoint) {
       var client = this.runtime.newClient();
-      var async = $.Deferred();
-      client.bindTo(this.runtime.getContext(), endpoint, function(err, client) {
-        if (err)
-          async.reject(err);
-        else
-          async.resolve(client);
-      });
-      
-      return async.promise();
+      return client.bindTo(this.runtime.getContext(), endpoint);
     },
-    
+
     /**
      * @param endpoint Vanadium name
      * @param server object implementing server APIs
      * @returns a promise resolving to void or rejecting with an error.
      */
-    server: function(endpoint, server, callback) {
-      var async = $.Deferred();
-      this.runtime.newServer().serve(endpoint, server, function(err) {
-        if (err)
-          async.reject(err);
-        else
-          async.resolve();
-      });
-      return async.promise();
+    server: function(endpoint, server) {
+      return this.runtime.newServer().serve(endpoint, server);
     }
   },
-  
+
   events: {
-    crash: 'memory'
+    onCrash: 'memory'
   }
 });
 
 module.exports = {
   /**
    * @param vanadium optional vanadium override
-   * @returns a promise resolving to a VanadiumWrapper or rejecting with an error.
+   * @returns a promise resolving to a VanadiumWrapper or rejecting with an
+   *  error.
    */
   init: function(vanadium) {
     vanadium = vanadium || vanadiumDefault;
 
     var config = {
       logLevel: vanadium.vlog.levels.INFO,
-      appName: 'Google Travel'
+      appName: 'Travel Planner'
     };
-    
+
     var async = $.Deferred();
-    
+
     vanadium.init(config, function(err, runtime) {
-      if (err)
+      if (err) {
         async.reject(err);
-      else
+      } else {
         async.resolve(new VanadiumWrapper(runtime));
+      }
     });
-    
+
     return async.promise();
   }
 };
diff --git a/test/components/maps.js b/test/components/maps.js
index e578bf0..013661b 100644
--- a/test/components/maps.js
+++ b/test/components/maps.js
@@ -1,7 +1,6 @@
 var test = require('tape');
 
 var $ = require('../../src/util/jquery');
-var defineClass = require('../../src/util/define-class');
 
 var Maps = require('../../src/components/maps');
 var message = require ('../../src/components/message');
@@ -10,18 +9,18 @@
 
 test('message display', function(t) {
   var maps = new Maps(mockMaps);
-  
+
   var $messages = $('.messages', maps.$);
   t.ok($messages.length, 'message display exists');
   t.equals($messages.children().length, 0, 'message display is empty');
-  
+
   maps.message(message.info('Test message.'));
-  
+
   var $messageItem = $messages.children();
   t.equals($messageItem.length, 1, 'message display shows 1 message');
   t.equals($messageItem.text(), 'Test message.',
     'message displays message text');
-    
+
   t.end();
 });
 
diff --git a/test/components/message.js b/test/components/message.js
index 3105f1b..16f64af 100644
--- a/test/components/message.js
+++ b/test/components/message.js
@@ -1,5 +1,4 @@
 var test = require('tape');
-var $ = require('../../src/util/jquery');
 
 var message = require('../../src/components/message');
 
@@ -14,13 +13,13 @@
   t.equal(msg.$[0].tagName, 'LI', 'tag name');
   t.assert(msg.$.hasClass('info'), 'class info');
   t.equal(msg.$.text(), 'Hello, world!', 'text');
-  
+
   msg.setType(message.ERROR);
   t.notOk(msg.$.hasClass('info'), 'class not info');
   t.assert(msg.$.hasClass('error'), 'class error');
-  
+
   msg.setText('hi');
   t.equal(msg.$.text(), 'hi', 'text update');
-  
+
   t.end();
 });
\ No newline at end of file
diff --git a/test/identity.js b/test/identity.js
index adb82de..814a810 100644
--- a/test/identity.js
+++ b/test/identity.js
@@ -1,5 +1,3 @@
-'use strict';
-
 var test = require('tape');
 
 var Identity = require('../src/identity');
@@ -45,7 +43,7 @@
   t.equals(i.username, 'joeuser@google.com',
     'should extract a username from a dev.v.io account name');
   var expectedPrefix = 'joeuser@google.com/desktop_';
-  t.assert(i.entityName.slice(0, expectedPrefix.length) == expectedPrefix,
+  t.assert(i.entityName.slice(0, expectedPrefix.length) === expectedPrefix,
     'entityName starts with expected prefix');
   t.assert(i.entityName.length > expectedPrefix.length,
     'entityName is longer than expected prefix');
diff --git a/test/travel.js b/test/travel.js
index 88eae30..a341a02 100644
--- a/test/travel.js
+++ b/test/travel.js
@@ -11,9 +11,11 @@
 }
 
 test('init', function(t) {
+  /* jshint -W031 */ //instantiation smoke test
   new Travel({
     maps: mockMaps
   });
+  /* jshint +W031 */
   t.end();
   cleanDom();
 });
@@ -23,18 +25,18 @@
     vanadiumWrapper: mockVanadiumWrapper,
     maps: mockMaps
   });
-  
+
   var $messages = $('.messages');
   t.ok($messages.length, 'message display exists');
   t.equals($messages.children().length, 0, 'message display is empty');
-  
+
   travel.info('Test message.');
-  
+
   var $messageItem = $messages.children();
   t.equals($messageItem.length, 1, 'message display shows 1 message');
   t.equals($messageItem.text(), 'Test message.',
     'message displays message text');
-    
+
   t.end();
   cleanDom();
 });
@@ -43,15 +45,17 @@
   var $root = $('<div>');
   var root = $root[0];
   $('body').append($root);
-  
+
+  /* jshint -W031 */ //top-level application
   new Travel({
     maps: mockMaps,
     vanadiumWrapper: mockVanadiumWrapper,
     domRoot: root
   });
-  
+  /* jshint +W031 */
+
   t.ok($root.children().length, 'app parented to given root');
-  
+
   t.end();
   cleanDom();
 });
\ No newline at end of file
diff --git a/test/util/define-class.js b/test/util/define-class.js
index 743d84c..68c6382 100644
--- a/test/util/define-class.js
+++ b/test/util/define-class.js
@@ -30,11 +30,11 @@
     constants: ['greeting'],
     events: ['stringQueried', {stringQueriedOnce: 'once'}]
   });
-  
+
   var testInstance = new TestClass('world');
-  
+
   t.ok(testInstance, 'instance instantiated');
-  
+
   var queried = 0, queriedOnce = 0;
   testInstance.stringQueried.add(function(value) {
     t.equal(value, 'world', 'event argument');
@@ -44,26 +44,26 @@
     t.equal(value, 'world', 'event argument');
     queriedOnce++;
   });
-  
+
   t.notOk(testInstance.stringQueried.fired(), 'event not fired');
-  
+
   t.equal(testInstance.greeting, 'Hello', 'public constant accessible');
   t.equal(testInstance.toString(), 'Hello, world!', 'public member accessible');
-  
+
   t.assert(testInstance.stringQueried.fired(), 'event fired');
-  
+
   t.equal(queried, 1, 'event fired');
   t.equal(queriedOnce, 1, 'once event fired');
-  
+
   testInstance.toString();
   t.equal(queried, 2, 'event fired again');
   t.equal(queriedOnce, 1, 'once event not fired again');
-  
+
   t.notOk(testInstance.getMessage, 'private member not accessible');
   t.notOk(testInstance.value, 'instance field not accessible');
   t.notOk(testInstance.stringQueried.fire, 'event fire not accessible');
   t.notOk(testInstance.stringQueried.fireWith, 'event fireWith not accessible');
-  
+
   t.end();
 });
 
@@ -71,7 +71,7 @@
   var TestClass = defineClass({
     init: function() {
       this.privateFires = this.publicFires = 0;
-      
+
       var self = this;
       this.privateEvent.add(function() {
         self.privateFires++;
@@ -80,16 +80,16 @@
         self.publicFires++;
       });
     },
-    
+
     publics: {
       getPrivateFires: function() {
         return this.privateFires;
       },
-      
+
       getPublicFires: function() {
         return this.publicFires;
       },
-      
+
       trigger: function() {
         this.triggerOnce.fire();
         this.privateEvent();
@@ -102,27 +102,27 @@
       publicEvent: 'public'
     }
   });
-  
+
   var testInstance = new TestClass();
   var count = 0;
   testInstance.triggerOnce.add(function() {
     count++;
   });
-  
+
   testInstance.trigger();
   t.equal(count, 1, 'event fired');
   testInstance.trigger();
   t.equal(count, 1, 'event not fired again');
-  
+
   t.notOk(testInstance.privateEvent, 'private event not accessible');
   t.equal(testInstance.getPrivateFires(), 2, 'private event fired twice');
   t.equal(testInstance.getPublicFires(), 2, 'public event fired twice');
-  
+
   t.notOk($.isFunction(testInstance.triggerOnce), 'normal event not callable');
   t.ok($.isFunction(testInstance.publicEvent), 'public event callable');
   testInstance.publicEvent();
   t.equal(testInstance.getPublicFires(), 3, 'public event fired thrice');
-  
+
   t.end();
 });
 
@@ -133,18 +133,56 @@
         return this.CONSTANT;
       }
     },
-    
+
     statics: {
       CONSTANT: 42
     }
   });
-  
+
   t.equal(TestClass.CONSTANT, 42, 'public static access');
-  
+
   var testInstance = new TestClass();
-  
+
   t.equal(testInstance.CONSTANT, 42, 'public access');
   t.equal(testInstance.getValue(), 42, 'private access');
-  
+
+  t.end();
+});
+
+test('inner class', function(t) {
+  var Outer = defineClass({
+    init: function(prefix) {
+      this.prefix = prefix;
+    },
+
+    publics: {
+      inner: defineClass.innerClass({
+        init: function(suffix) {
+          this.suffix = suffix;
+        },
+
+        publics: {
+          getString: function() {
+            return this.outer.prefix + this.suffix;
+          }
+        }
+      })
+    }
+  });
+
+  var outer1 = new Outer('Hello, ');
+  var inner1x1 = outer1.inner('world!'),
+      inner1x2 = outer1.inner('Cleveland!');
+
+  var outer2 = new Outer('Goodnight, ');
+  var inner2 = outer2.inner('moon.');
+
+  t.equal(inner1x1.getString(), 'Hello, world!',
+    'fields of outer and inner classes not corrupted by other instances');
+  t.equal(inner1x2.getString(), 'Hello, Cleveland!',
+    'multiple instances of an inner class');
+  t.equal(inner2.getString(), 'Goodnight, moon.',
+    'multiple instances of an outer class');
+
   t.end();
 });
diff --git a/test/vanadium-wrapper.js b/test/vanadium-wrapper.js
index 43d1427..09ad34d 100644
--- a/test/vanadium-wrapper.js
+++ b/test/vanadium-wrapper.js
@@ -1,8 +1,5 @@
 var test = require('tape');
 
-var $ = require('../src/util/jquery');
-var defineClass = require('../src/util/define-class');
-
 var vanadiumWrapper = require('../src/vanadium-wrapper');
 
 var vanadiumMocks = require('../mocks/vanadium');
@@ -12,11 +9,11 @@
 function setUpCrashTest(t) {
   var mockVanadium = new MockVanadium(t);
   var mockRuntime = new MockRuntime();
-  
+
   var context = {
     bindCrashHandler: function(err) {
       var self = this;
-      self.vanadiumWrapper.crash.add(function(err) {
+      self.vanadiumWrapper.onCrash.add(function(err) {
         self.crashErr = err;
       });
     },
@@ -24,7 +21,7 @@
       mockRuntime.fireCrash(err);
     }
   };
-  
+
   vanadiumWrapper.init(mockVanadium).then(
     function(v) {
       context.vanadiumWrapper = v;
@@ -32,19 +29,19 @@
     function(err) {
       t.fail('init error');
     });
-  
+
   mockVanadium.finishInit(null, mockRuntime);
-  
+
   return context;
 }
 
 test('crashBefore', function(t) {
   var crashTest = setUpCrashTest(t);
-  
+
   crashTest.crash('I lost the game.');
   crashTest.bindCrashHandler();
   t.equal(crashTest.crashErr, 'I lost the game.');
-  
+
   t.end();
 });
 
@@ -52,9 +49,9 @@
   var crashTest = setUpCrashTest(t);
   crashTest.bindCrashHandler();
   t.notOk(crashTest.crashErr, 'no crash yet');
-  
+
   crashTest.crash('I lost the game.');
   t.equal(crashTest.crashErr, 'I lost the game.');
-  
+
   t.end();
 });
\ No newline at end of file