reader: adds modules + boilerplate for tests, and coverage

* Adds tape.
* Adds a simple component test that can run in browser or node.
* Adds helpers for setting up components and testing updates/input/render.
* Adds disc to help inspect the size impact of modules on public/bundle.js.
* Adds a coverage tool.

Closes #2

Change-Id: I3429b1848cd2c9b0ba3f6fb533e31e692b90446d
diff --git a/.gitignore b/.gitignore
index 018dc7e..d86576c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
 node_modules
 npm-debug.log
 public/bundle.*
+coverage
+disc.html
diff --git a/.jshintignore b/.jshintignore
index 312a3a0..0d4db1c 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,2 +1,3 @@
 node_modules
 public/*.js
+coverage
diff --git a/.jshintrc b/.jshintrc
index 5d863c8..44fcb6d 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -25,6 +25,8 @@
 
   "globals": {
     "PDFJS": false,
-    "window": true
+    "window": true,
+    "event": true,
+    "document": true
   }
 }
diff --git a/Makefile b/Makefile
index a0f2f90..ba5fb96 100644
--- a/Makefile
+++ b/Makefile
@@ -36,8 +36,15 @@
 	@jshint .
 
 .PHONY:
-test: lint all
-	@true
+test: lint node_modules
+	tape test/index.js
+
+coverage: $(js_files) node_modules
+	@istanbul cover --report html --print detail ./test/index.js
+	@touch coverage
+
+disk.html: browser/index.js $(js_files) node_modules
+	browserify --full-paths $< | discify > $@
 
 .PHONY:
 start: all
diff --git a/browser/components/vanadium/index.js b/browser/components/peers/index.js
similarity index 92%
rename from browser/components/vanadium/index.js
rename to browser/components/peers/index.js
index ba72ea1..8d632a0 100644
--- a/browser/components/vanadium/index.js
+++ b/browser/components/peers/index.js
@@ -7,6 +7,6 @@
 // Wraps Vanadium functionality in a mercury style component so that state can
 // be easily modified, observed, and rendered into the UI as appropriate.
 module.exports = {
-  state: require('./state'),
+  create: require('./state'),
   render: require('./render')
 };
diff --git a/browser/components/vanadium/render.js b/browser/components/peers/render.js
similarity index 66%
rename from browser/components/vanadium/render.js
rename to browser/components/peers/render.js
index c07d7c5..7e21473 100644
--- a/browser/components/vanadium/render.js
+++ b/browser/components/peers/render.js
@@ -2,8 +2,10 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var hg = require('mercury');
 var h = require('mercury').h;
 var format = require('format');
+var uuid = require('uuid');
 
 module.exports = render;
 
@@ -13,7 +15,14 @@
     h('li', format('status: %s', state.status)),
     h('li', format('error: %s', state.error ? state.error.message : 'none')),
     h('ul.peers', Object.keys(state.peers).map(function(key) {
-      return h('li', format('peer: %s', key));
-    }))
+      return h('li.peer', {
+        'data-id': key
+      }, format('peer: %s', key));
+    })),
+    h('li', [
+      h('a.add-peer', {
+        'ev-click': hg.send(channels.add, { id: uuid.v4() })
+      }, 'Add peer')
+    ])
   ]);
 }
diff --git a/browser/components/peers/state.js b/browser/components/peers/state.js
new file mode 100644
index 0000000..a0e6807
--- /dev/null
+++ b/browser/components/peers/state.js
@@ -0,0 +1,30 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var debug = require('debug')('reader:vanadium:state');
+var hg = require('mercury');
+var uuid = require('uuid');
+
+module.exports = create;
+
+// Create state for the Vanadium component
+function create(options) {
+  debug('initialize state');
+
+  var state = hg.state({
+    error: hg.value(null),
+    id: hg.value(uuid.v4()),
+    status: hg.value('new'),
+    peers: hg.varhash({}),
+    channels: {
+      add: add
+    }
+  });
+
+  return state;
+}
+
+function add(state, data) {
+  state.peers.put(data.id, data);
+}
diff --git a/browser/components/vanadium/state.js b/browser/components/vanadium/state.js
deleted file mode 100644
index b0ba929..0000000
--- a/browser/components/vanadium/state.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var vanadium = require('../../vanadium');
-var debug = require('debug')('reader:vanadium:state');
-var hg = require('mercury');
-var uuid = require('uuid');
-
-module.exports = create;
-
-// Create state for the Vanadium component
-function create(options) {
-  debug('initialize state');
-
-  var state = hg.state({
-    error: hg.value(null),
-    id: hg.value(null),
-    status: hg.value(''),
-    peers: hg.varhash({}),
-    channels: {
-    }
-  });
-
-  var config = {
-    app: 'reader-example',
-    id: uuid.v4()
-  };
-
-  vanadium(config)
-  .on('error', function(err) {
-    state.error.set(err);
-  })
-  .on('status', function(status) {
-    state.status.set(status);
-  })
-  .on('name', function(name) {
-    state.id.set(name);
-  })
-  .on('connect', function(name, service) {
-    debug('connect :D %s %o', name, service);
-    // NOTE: objects in the varhash might be better off as components depending
-    // on how they need to be interacted with...
-    state.peers.put(name, service);
-  });
-
-  return state;
-}
diff --git a/browser/index.js b/browser/index.js
index 0cc120a..9d3172c 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -11,7 +11,7 @@
 var filePicker = require('./components/file-picker');
 var pageControls = require('./components/page-control');
 var pdf = require('./components/pdf');
-var vanadium = require('./components/vanadium');
+var peers = require('./components/peers');
 
 domready(function ondomready() {
   debug('domready');
@@ -20,12 +20,12 @@
   var state = hg.state({
     pdf: pdf.state(),
     pageControls: pageControls.state(),
-    vanadium: vanadium.state()
+    peers: peers.create()
   });
 
   // TODO(jasoncampbell): add an error component for aggregating, logging, and
   // displaying errors in the UI.
-  state.vanadium.error(function(err) {
+  state.peers.error(function(err) {
     throw err;
   });
 
@@ -45,6 +45,10 @@
     }
   });
 
+  // TODO(jasoncampbell): Add/couple Vanadium functionality to the state here
+  // instead of inside the peers component so that async paths which are hard to
+  // test/stub can be isolated to the application initialization.
+
   hg.app(document.body, state, render);
 });
 
@@ -52,7 +56,7 @@
   if (state.pdf.pdf === null) {
     return h('div', [
       hg.partial(filePicker.render, state.pdf, state.pdf.channels),
-      hg.partial(vanadium.render, state.vanadium, state.vanadium.channels)
+      hg.partial(peers.render, state.peers, state.peers.channels)
     ]);
   } else {
     return h('div', [
@@ -61,7 +65,7 @@
           state.pageControls,
           state.pageControls.channels),
       hg.partial(pdf.render, state.pdf, state.pdf.channels),
-      hg.partial(vanadium.render, state.vanadium, state.vanadium.channels)
+      hg.partial(peers.render, state.peers, state.peers.channels)
     ]);
   }
 }
diff --git a/browser/vanadium/index.js b/browser/vanadium/index.js
index 751edef..919580b 100644
--- a/browser/vanadium/index.js
+++ b/browser/vanadium/index.js
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var window = require('global/window');
 var debug = require('debug')('reader:vanadium-wrapper');
 var vanadium = require('vanadium');
 var inherits = require('inherits');
@@ -71,7 +72,9 @@
     wrapper.emit('runtime', runtime);
     wrapper.emit('status', 'initialized');
 
-    window.addEventListener('beforeunload', beforeunload);
+    if (window.addEventListener) {
+      window.addEventListener('beforeunload', beforeunload);
+    }
 
     var server = runtime.newServer();
     var name = wrapper.name();
diff --git a/package.json b/package.json
index 47941e8..cf615b0 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,12 @@
   "license": "BSD",
   "devDependencies": {
     "browserify": "^10.2.4",
+    "disc": "^1.3.2",
+    "istanbul": "^0.3.17",
     "jshint": "^2.8.0",
-    "proxyquire": "^1.5.0",
+    "raf": "^3.0.0",
     "st": "^0.5.4",
+    "synthetic-dom-events": "git://github.com/Raynos/synthetic-dom-events",
     "tape": "^4.0.0"
   },
   "dependencies": {
diff --git a/test/components/dispatch.js b/test/components/dispatch.js
new file mode 100644
index 0000000..bf2f668
--- /dev/null
+++ b/test/components/dispatch.js
@@ -0,0 +1,21 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var event = require('synthetic-dom-events');
+var query = require('./query');
+
+module.exports = dispatch;
+
+function dispatch(type, selector) {
+  var nodes = query(selector);
+
+  if (! (nodes instanceof Array)) {
+    nodes = [ nodes ];
+  }
+
+  var length = nodes.length;
+  for (var i = 0; i < length; i++) {
+    nodes[i].dispatchEvent(event(type));
+  }
+}
diff --git a/test/components/query.js b/test/components/query.js
new file mode 100644
index 0000000..b028998
--- /dev/null
+++ b/test/components/query.js
@@ -0,0 +1,41 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var document = require('global/document');
+var separators = [ '.', '#' ];
+
+module.exports = query;
+
+function query(selector) {
+  if (typeof selector !== 'string') {
+    throw new Error('selector must be a string');
+  }
+
+  // TODO(jasoncampbell): guard against selectors that go more than one depth.
+
+  var separator;
+  var length = separators.length;
+  for (var i = 0; i < length; i++) {
+    if (selector.indexOf(separators[i]) > -1) {
+      separator = separators[i];
+      break;
+    }
+  }
+
+  var results;
+  switch (separator) {
+    case '.':
+      var className = selector.replace(separator, '');
+      results = document.getElementsByClassName(className);
+      break;
+    case '#':
+      var id = selector.replace(separator, '');
+      results = document.getElementById(id);
+      break;
+    default:
+      results = document.getElementsByTagName(selector);
+  }
+
+  return results;
+}
diff --git a/test/components/setup.js b/test/components/setup.js
new file mode 100644
index 0000000..570c31c
--- /dev/null
+++ b/test/components/setup.js
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var document = require('global/document');
+var hg = require('mercury');
+
+module.exports = setup;
+
+// Wraps up functioanility for embedding a component in the document and setting
+// up some test cleanup to run after t.end() is called.
+//
+// SEE: http://git.io/vmR3O
+function setup(component, callback) {
+  var div = document.createElement('div');
+  document.body.appendChild(div);
+
+  var state = component.create();
+  var initial = state();
+  var remove = hg.app(div, state, render);
+
+  return function fn(t) {
+    t.once('end', function() {
+      state.set(initial);
+      document.body.removeChild(div);
+      remove();
+    });
+
+    callback(t, state);
+  };
+
+  function render(state) {
+    return hg.partial(component.render, state, state.channels);
+  }
+}
diff --git a/test/components/test-peers.js b/test/components/test-peers.js
new file mode 100644
index 0000000..ea743a0
--- /dev/null
+++ b/test/components/test-peers.js
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var test = require('tape');
+var component = require('../../browser/components/peers');
+var raf = require('raf');
+var query = require('./query');
+var setup = require('./setup');
+var dispatch = require('./dispatch');
+
+test('components/peers - state', function(t) {
+  var state = component.create();
+
+  t.ok(state.id(), 'state should have id');
+  t.equal(state.status(), 'new');
+  t.same(state.peers(), {});
+  t.end();
+});
+
+test('components/peers - fake test', setup(component, function(t, state){
+  t.same(state.peers(), {});
+
+  dispatch('click', '.add-peer');
+
+  t.notSame(state.peers(), {});
+
+  raf(function() {
+    var nodes = query('.peer');
+    var element = nodes[0];
+
+    t.equal(nodes.length, 1);
+    t.ok(element['data-id'], 'should have data-id attribute');
+    t.equal(element.childNodes[0].data, 'peer: ' + element['data-id']);
+    t.end();
+  });
+}));
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..20ac3fe
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,5 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+require('./components/test-peers');