Add a bunch of unit tests (not nearly complete, but it's a start).
diff --git a/edit.html b/edit.html
index 9be9bda..8e753ba 100644
--- a/edit.html
+++ b/edit.html
@@ -60,7 +60,7 @@
   for (var parti in parts) {
     var part = parts[parti];
     if (part) {
-      var bits = afterquery.trySplitOne(part, '=');
+      var bits = afterquery.internal.trySplitOne(part, '=');
       if (bits) {
         out.push(niceEncode(bits[0]) + '=' + niceEncode(bits[1]));
       } else {
@@ -90,7 +90,8 @@
 lastloaded = null;
 var exampleurl = (window.location.protocol + '//' + window.location.host +
                   '/example1.json');
-args = afterquery.parseArgs(window.location.search || ('?url=' + exampleurl));
+args = afterquery.internal.parseArgs(window.location.search ||
+                                     ('?url=' + exampleurl));
 argsToForm(args);
 updateUrl();
 </script>
diff --git a/render.js b/render.js
index b6c2e43..faaa028 100644
--- a/render.js
+++ b/render.js
@@ -1,11 +1,25 @@
 'use strict';
 
 var afterquery = (function() {
-  // Mostly for konqueror compatibility
-  var console = window.console;
+  // To appease v8shell
+  var console;
+  try {
+    console = window.console;
+  }
+  catch (ReferenceError) {
+    console = {
+      debug: print
+    };
+  }
+
+  // For konqueror compatibility
   if (!console) {
-    console = {};
-    console.debug = function() {};
+    console = window.console;
+  }
+  if (!console) {
+    console = {
+      debug: function() {}
+    };
   }
 
 
@@ -92,12 +106,11 @@
 
 
   function guessTypes(data) {
-    console.debug('guessTypes');
     var impossible = [];
     for (var rowi in data) {
       var row = data[rowi];
       for (var coli in row) {
-        impossible[coli] += 0;
+        impossible[coli] |= 0;
         var cell = row[coli];
         if (cell == '' || cell == null) continue;
         var d = myParseDate(cell);
@@ -136,12 +149,13 @@
 
   var DATE_RE1 = RegExp('^(\\d{4})[-/](\\d{1,2})(?:[-/](\\d{1,2})' +
                         '(?:[T\\s](\\d{1,2}):(\\d\\d)(?::(\\d\\d))?)?)?$');
-  var DATE_RE2 = RegExp('^Date\\((\\d+),(\\d+),(\\d+)' +
-                        '(?:,(\\d+),(\\d+)(?:,(\\d+)(?:,(\\d+))?)?)?\\)$');
+  var DATE_RE2 = /^Date\(([\d,]+)\)$/;
   function myParseDate(s) {
     if (s == null) return s;
     if (s && s.getDate) return s;
-    var g = DATE_RE1.exec(s) || DATE_RE2.exec(s);
+    var g = DATE_RE2.exec(s);
+    if (g) g = (',' + g[1]).split(',');
+    if (!g || g.length > 8) g = DATE_RE1.exec(s);
     if (g) {
       return new Date(g[1], g[2] - 1, g[3] || 1,
                       g[4] || 0, g[5] || 0, g[6] || 0, g[7] || 0);
@@ -1119,8 +1133,22 @@
   }
 
   return {
-    parseArgs: parseArgs,
-    trySplitOne: trySplitOne,
+    internal: {
+      parseArgs: parseArgs,
+      trySplitOne: trySplitOne,
+      dataToGvizTable: dataToGvizTable,
+      guessTypes: guessTypes,
+      groupBy: groupBy,
+      pivotBy: pivotBy,
+      stringifiedCols: stringifiedCols,
+      treeify: treeify,
+      filterBy: filterBy,
+      queryBy: queryBy,
+      orderBy: orderBy,
+      extractRegexp: extractRegexp,
+      fillNullsWithZero: fillNullsWithZero,
+      gridFromData: gridFromData
+    },
     render: wrap(_run)
   };
 })();
diff --git a/t/trender.js b/t/trender.js
new file mode 100644
index 0000000..40a8ead
--- /dev/null
+++ b/t/trender.js
@@ -0,0 +1,115 @@
+'use strict';
+
+
+function dump(arg) {
+  function _dump(indent, obj) {
+    var any = 0;
+    if (typeof obj != 'string') {
+      for (var i in obj) {
+        _dump((indent ? indent + '.' : '') + i, obj[i]);
+        any++;
+      }
+    }
+    if (!any) {
+      print(indent + ' = ' + obj);
+    }
+  }
+  _dump('', arg);
+}
+
+
+// load the 'afterquery' object for testing
+load('render.js');
+
+
+// fake gviz library implementation
+var google = {
+  visualization: {
+    DataTable: function(t) { return t; }
+  }
+};
+
+wvtest('parseArgs', function() {
+  var query = '?a=b&c=d&e==f&&g=h&g=i%25%31%31&a=&a=x';
+  var args = afterquery.internal.parseArgs(query);
+  WVPASSEQ(args.all.join('|'), 'a,b|c,d|e,=f|,|g,h|g,i%11|a,|a,x');
+  WVPASSEQ(args.get('a'), 'x');
+  WVPASSEQ(args.get(''), '');
+  WVPASSEQ(args.get('g'), 'i%11');
+
+  WVPASSEQ(afterquery.internal.parseArgs('').all.join('|'), ',');
+  WVPASSEQ(afterquery.internal.parseArgs('?').all.join('|'), ',');
+  WVPASSEQ(afterquery.internal.parseArgs('abc=def').all.join('|'), 'bc,def');
+});
+
+
+wvtest('dataToGvizTable', function() {
+  var grid = {
+    headers: ['a', 'b', 'c', 'd', 'e'],
+    types: ['number', 'date', 'datetime', 'bool', 'string'],
+    data: [
+      [null, null, null, null, null],
+      [0, 1, 2, 3, 4],
+      [1.5, '2012-11-15 01:23', '2013-12-16 01:24:25', false, 'hello']
+    ]
+  };
+  var dt = afterquery.internal.dataToGvizTable(grid, {});
+  dump(dt);
+  WVPASSEQ(dt.cols.length, 5);
+  WVPASSEQ(dt.rows.length, 3);
+  for (var i in dt.cols) {
+    WVPASSEQ(dt.cols[i].id, ['a', 'b', 'c', 'd', 'e'][i]);
+    WVPASSEQ(dt.cols[i].label, dt.cols[i].id);
+    WVPASSEQ(dt.cols[i].type,
+             ['number', 'date', 'datetime', 'bool', 'string'][i]);
+  }
+  for (var coli in dt.rows[0]) {
+    print('row', 0, 'col', coli);
+    WVPASSEQ(dt.rows[0].c[coli], null);
+  }
+  for (var rowi = 1; rowi < dt.rows.length; rowi++) {
+    for (var coli in dt.rows[rowi].c) {
+      print('row', rowi, 'col', coli);
+      WVPASSEQ(dt.rows[rowi].c[coli].v, grid.data[rowi][coli]);
+    }
+  }
+});
+
+
+wvtest('guessTypes', function() {
+  var data1 = [['1999-01-01', '1999-02-02', 1, 2.5, false, 'foo']];
+  var data2 = [['1999-01-01', '1999-02-02 12:34', 2, 'x', true, null]];
+  var datanull = [[null, null, null, null]];
+  var guessTypes = afterquery.internal.guessTypes;
+  WVPASSEQ(guessTypes([]), []);
+  WVPASSEQ(guessTypes([[5]]), ['number']);
+  WVPASSEQ(guessTypes([[null]]), ['boolean']);
+  WVPASSEQ(guessTypes([['2012']]), ['number']);
+  WVPASSEQ(guessTypes([['2012-01']]), ['date']);
+  WVPASSEQ(guessTypes([['2012/01']]), ['date']);
+  WVPASSEQ(guessTypes([['2012/01-02']]), ['date']);
+  WVPASSEQ(guessTypes([['2012/01/01 23:45']]), ['datetime']);
+  WVPASSEQ(guessTypes([['2012-01/01 23:45:67']]), ['datetime']);
+  WVPASSEQ(guessTypes([['2012/01/01T23:45:67']]), ['datetime']);
+  WVPASSEQ(guessTypes([['2012-01-01T23:45:67']]), ['datetime']);
+  WVPASSEQ(guessTypes([['2012/01/01 23:45:67.12']]), ['string']);
+  WVPASSEQ(guessTypes([['Date(2012,2,3)']]), ['date']);
+  WVPASSEQ(guessTypes([['Date(2012,2,3,4)']]), ['datetime']);
+  WVPASSEQ(guessTypes([['Date(2012,2,3,4,5,6)']]), ['datetime']);
+  WVPASSEQ(guessTypes([['Date(2012,2,3,4,5,6,7)']]), ['datetime']);
+  WVPASSEQ(guessTypes([['Date(2012,2,3,4,5,6,7,8)']]), ['string']);
+  WVPASSEQ(guessTypes([['Date(2012,x,1)']]), ['string']);
+  WVPASSEQ(guessTypes(data1),
+           ['date', 'date', 'boolean', 'number', 'boolean', 'string']);
+  WVPASSEQ(guessTypes(data2),
+           ['date', 'datetime', 'number', 'string', 'boolean', 'boolean']);
+  WVPASSEQ(guessTypes(data1.concat(data2)),
+           ['date', 'datetime', 'number', 'string', 'boolean', 'string']);
+  WVPASSEQ(guessTypes(data2.concat(data1)),
+           ['date', 'datetime', 'number', 'string', 'boolean', 'string']);
+  WVPASSEQ(guessTypes(data2.concat(datanull)
+                                          .concat(data1)),
+           ['date', 'datetime', 'number', 'string', 'boolean', 'string']);
+  WVPASSEQ(guessTypes(data2.concat(datanull)),
+           ['date', 'datetime', 'number', 'string', 'boolean', 'boolean']);
+});
diff --git a/wvtest.js b/wvtest.js
index e7b1d92..3a1008c 100644
--- a/wvtest.js
+++ b/wvtest.js
@@ -92,6 +92,10 @@
 
 function WVPASSEQ(a, b, precision) {
     var t = trace()[1];
+    if (a && b && a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
     var cond = precision ? Math.abs(a-b) < precision : (a == b);
     return _check(cond, t, '' + a + ' == ' + b);
 }
@@ -99,6 +103,10 @@
 
 function WVPASSNE(a, b, precision) {
     var t = trace()[1];
+    if (a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
     var cond = precision ? Math.abs(a-b) >= precision : (a != b);
     return _check(a != b, t, '' + a + ' != ' + b);
 }