Skip to content

Instantly share code, notes, and snippets.

@johan
Created November 26, 2012 06:07
Show Gist options
  • Select an option

  • Save johan/4146798 to your computer and use it in GitHub Desktop.

Select an option

Save johan/4146798 to your computer and use it in GitHub Desktop.
tap-grid.js

tap-grid

This little utility is for taking the output of your testem test suite continuous integration results across a few browsers, and turning it into a nice grid style overview, a bit like I did with this SVG test suite. (related testem issue)

You run it like this:

testem --timeout 60 ci | tap-grid.js

Which gives you the array of test names, the json for the success (0) / fail ({id, message, stacktrace} or {id, raw}) grid and the array of browser names participating.

Then you finish up the d3 integration. :-)

colgroup {
width: 22px;
}
.grid tbody.data tr {
height: 20px;
}
.grid thead {
vertical-align: bottom;
}
.grid,
.grid tbody th:nth-child(even) {
background: #f0f0f0;
}
.grid .data tr:nth-child(odd) td:nth-child(odd) {
background-color: #e0e0e0;
}
.grid tbody tr:nth-child(even) th,
.grid thead th:nth-child(even),
.grid .data tr:nth-child(even) td:nth-child(even) {
background-color: #fff;
}
/* test names */
.data tr th a {
white-space: pre;
font-family: monaco, monospace;
font-size: 8px;
}
/* horizontal separators between specific sections of the grid: */
.group { border-top:1px solid #444; }
/* in-grid pills */
td.none, td.pass, td.warn, td.fail {
height: 22px;
min-width: 22px;
}
/* pills legend */
thead th td {
font-weight: normal;
white-space: nowrap;
height: 38px;
min-width: 22px;
}
#chart > thead > tr {
height: 140px;
}
#chart thead table {
margin: 0 auto .75em .25em;
}
#chart thead th td {
height: 32px;
text-align: left;
}
/* standing headers */
#chart thead th {
height: 100px;
}
#chart th div {
width: 100px;
-webkit-transform-origin: 0% 100%;
-webkit-transform: rotate(-90deg);
-moz-transform-origin: 0% 100%;
-moz-transform: rotate(-90deg);
-ms-transform-origin: 0% 100%;
-ms-transform: rotate(-90deg);
-o-transform-origin: 0% 100%;
-o-transform: rotate(-90deg);
transform-origin: 0% 100%;
transform: rotate(-90deg);
white-space: nowrap;
max-width: 24px;
margin-left: 20px;
margin-right: -20px;
margin-bottom: 4px;
position: relative;
font-weight: normal;
}
#chart th, #chart th div, #chart th a {
color: #444;
text-decoration: none;
}
#chart thead th.hover div, #chart thead th.hover a {
color: #000;
}
#chart thead th.hover span {
opacity: 1;
}
#chart thead th div span {
font-size: 10px;
opacity: 0.6;
position: absolute;
display: inline-block;
left: 80px;
top: 3px;
}
#chart .data th {
font-weight: normal;
white-space: nowrap;
text-align: left;
font-size: 14px;
}
.data td.none, td.none { background-image: url(icons/lite.gif); }
.data td.pass, td.pass { background-image: url(icons/pass.gif); }
.data td.warn, td.warn { background-image: url(icons/warn.gif); }
.data td.fail, td.fail { background-image: url(icons/fail.gif); }
.data td.died, td.died { background-image: url(icons/fail.gif); }
.data td, #chart thead th table td {
background-repeat: no-repeat;
background-position: 50% 50%;
opacity: 0.75;
}
.data td:hover { opacity: 1; }
#chart .data tr:hover th a { color: #000; }
.data td.notes {
width: auto;
background-image: none;
}
<!DOCTYPE html>
<html><head>
<title>on.js test suite results by browser</title>
<link rel="stylesheet" type="text/css" href="grid.css">
<script src="http://d3js.org/d3.v2.js"></script>
</head>
<body>
<h1>on.js test suite results by browser</h1>
<p>
This is example data from <a href="http://git.io/on.js">on.js</a>, modded to
actually show a failure so it gets a little interesting. Generating your own
real data is easy: just run <code>testem ci | tap-grid.js > tap.json</code>
/ Johan Sundström
</p>
<table id="chart" class="grid" rules="groups" cellspacing="0" cellpadding="2">
<thead><tr>
<th><table border="0"><tbody>
<tr><td class="none"></td><td>Not tested</td></tr>
<tr><td class="pass"></td><td>Test passed</td></tr>
<!--tr><td class="warn"></td><td>See notes</td></tr-->
<tr><td class="fail"></td><td>Test failed</td></tr>
<!--tr><td class="died"></td><td>Test crashed!</td></tr-->
</tbody></table></th>
</thead>
</table>
<script src="vis.js"></script>
</body></html>
#! /usr/bin/env node
var util = require('util')
, stdin = process.stdin
, now = (new Date).toISOString().split('T')[0]
, error = ''
, line = ''
, line_no = 0
, comments = 0
, errors = 0
, b_count = -1 // # of browsers, total
, b_column = {} // browser name: its column index in results
, browsers = {} // browser name: its first test id
, okay_cnt = {} // browser name: count
, fail_cnt = {} // browser name: count
, testname = [] // test no: test name
, results = [] // one row per test, one column per browser
, tap_ver
, current // error object we are populating at the moment - or undefined
;
stdin.resume(); // http://nodejs.org/docs/v0.4.7/api/process.html#process.stdin
stdin.setEncoding('utf8');
stdin.on('data', gotChunk).on('end', reachedEOF);
function gotChunk(chunk) {
var lines = chunk.toString().split('\n');
lines[0] = line + lines[0];
line = lines.pop();
lines.forEach(gotLine);
}
function gotLine(line) {
++line_no;
if (line.charAt() === '#') {
//console.log(line);
++comments;
return;
}
if (!line) return; // ignore empty lines
// TAP version 13
if (!tap_ver) {
try { tap_ver = /^TAP version (\d+)$/.exec(line)[1]; } catch(e) {}
if (tap_ver && tap_ver != 13) {
console.warn('Warning: only tested with TAP 13 input!');
}
return;
}
// N..M = "ran tests N..M"?
if (/^\d+\.\.\d+$/.test(line)) return;
// ok 1 - <browser> <test name>
// not ok 1 - <browser> <test name>
// <indented continuation>
var bits = /^(\s*)(?:(not )?ok (\d+) - (\S+) (.*)|(.*))/.exec(line), ws;
if (bits) {
if ((ws = bits[1])) { // leading ws: error continuation?
error += ws + bits[6] + '\n';
}
else {
var failed = bits[2]
, g_number =+bits[3]
, browser = bits[4]
, t_name = bits[5]
, b_col = b_column[browser] = b_column[browser] == null ? ++b_count
: b_count
, b_start = browsers[browser] = browsers[browser] || g_number
, t_number = g_number - b_start
, test = results[t_number] = results[t_number] || [];
if (current) {
gotError(current, error);
current = undefined;
}
testname[t_number] = testname[t_number] || t_name;
if (failed) {
fail_cnt[browser] = (fail_cnt[browser] || 0) + 1;
test[b_col] = current = { id: g_number };
error = '';
}
else {
okay_cnt[browser] = (okay_cnt[browser] || 0) + 1;
test[b_col] = 0; // all okay
}
}
}
else {
console.error('Failed to parse line '+ line_no +': '+ line);
}
}
function gotError(error, raw) {
++errors;
if (raw.slice(0, 6) === ' ---\n') raw = raw.slice(6);
var msg = /^\s*message:\s*(.*\S)\s*\n([\S\s]*)/.exec(raw);
if (msg) {
error.message = msg[1];
raw = msg[2];
}
var stack = /^\s*stacktrace:[\s|]*\n([\S\s]*)/.exec(raw);
if (stack)
error.stacktrace = stack[1];
else if (raw !== ' ...\n') // =what PhantomJS lists instead of a strack trace
error.raw = raw;
}
function json(data) { return JSON.stringify(data); }
function test(name, i) { return { id: i + 1, name: name.split('\t ') }; }
function browser(name) { return { name: name, date: now }; }
function reachedEOF() {
if (line) gotLine(line); // trailing partial line?
var browsers = Object.keys(b_column).map(browser);
console.warn([ 'TAP: '+ tap_ver
, 'browsers: '+ browsers.length
, 'errors: '+ errors
, 'comments: '+ comments
, 'total lines: '+ line_no
].join(', '));
console.log('{"environments":'+ json(browsers) +'\n'
+',"tests":'+ json(testname.map(test)) +'\n'
+',"results":'+ json(results) +'\n'
+'}');
}
function present(what, data) {
console.log(what +':\n'+ data +'\n');
}
{"environments":[{"name":"Chrome","date":"2012-11-26"},{"name":"Firefox","date":"2012-11-26"},{"name":"Safari","date":"2012-11-26"},{"name":"Opera","date":"2012-11-26"},{"name":"PhantomJS","date":"2012-11-26"}]
,"tests":[{"id":1,"name":["on()","should throw an error on no input."]},{"id":2,"name":["on()","should expose an on.dom function after the first call."]},{"id":3,"name":["on()","should expose an on.query function after the first call."]},{"id":4,"name":["on()","should expose an on.path_re function after the first call."]},{"id":5,"name":["on()","should accept an object with \"path_re\", \"dom\", and/or \"query\" specs."]},{"id":6,"name":["on.dom(dom spec – see below for the three types of dom spec)","should be a function after the first on() call."]},{"id":7,"name":["on.dom(dom spec – see below for the three types of dom spec)","should expose on.dom.* functions once on.dom() has run once."]},{"id":8,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","Array of Node:s (0+ occurrences):","on.dom(\"css* NotFound\") => []."]},{"id":9,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","Array of Node:s (0+ occurrences):","on.dom(\"css* html\") => [html]."]},{"id":10,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","Array of Node:s (0+ occurrences):","on.dom(\"css* *\") => document.all (but as a proper Array)."]},{"id":11,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","Array of Node:s (1+ occurrences):","on.dom(\"css+ html\") => [html]."]},{"id":12,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","Array of Node:s (1+ occurrences):","on.dom(\"css+ NotFound\") => undefined."]},{"id":13,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","single optional Node, or null if not found:","on.dom(\"css? *\") => root element (= first match)."]},{"id":14,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","single optional Node, or null if not found:","on.dom(\"css? NotFound\") => null (not found)."]},{"id":15,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","single mandatory Node:","on.dom(\"css *\") => the root element."]},{"id":16,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"css… selection\"): Array/Node, optional/not?","single mandatory Node:","on.dom(\"css NotFound\") => undefined (unsatisfied)."]},{"id":17,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath* => Array of Node:s (0+ occurrences):","on.dom(\"xpath* /*\") => [root element]."]},{"id":18,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath* => Array of Node:s (0+ occurrences):","on.dom(\"xpath* /NotFound\") => []."]},{"id":19,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath+ => Array of Node:s (1+ occurrences):","on.dom(\"xpath+ /*\") => [root element]."]},{"id":20,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath+ => Array of Node:s (1+ occurrences):","on.dom(\"xpath+ /NotFound\") => undefined."]},{"id":21,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath? => single optional Node, or null if missing:","on.dom(\"xpath? /NotFound\") => null."]},{"id":22,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath? => single optional Node, or null if missing:","on.dom(\"xpath? /*\") => the root element."]},{"id":23,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath => single mandatory Node:","on.dom(\"xpath /*\") => the root element."]},{"id":24,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath => single mandatory Node:","on.dom(\"xpath /NotFound\") => undefined."]},{"id":25,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath => single mandatory Node:","on.dom(\"xpath .\") => the current document."]},{"id":26,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath count(/)\") => 1."]},{"id":27,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath count(/NotFound)\") => 0."]},{"id":28,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath name(/*)\") => \"html\" or \"HTML\"."]},{"id":29,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath name(/)\") => \"\"."]},{"id":30,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath count(/*) = 1\") => true."]},{"id":31,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","…or queries yielding Number/String/Boolean answers:","on.dom(\"xpath name(/*) = 'nope'\") => false."]},{"id":32,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! count(/)\") => 1."]},{"id":33,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! count(/NotFound)\") => undefined."]},{"id":34,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! name(/*)\") => \"html\"."]},{"id":35,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! name(/)\") => undefined."]},{"id":36,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! count(/*) = 1\") => true."]},{"id":37,"name":["on.dom(dom spec type 1: a selector string)","on.dom(\"xpath… selection\"): Array/Node, optional/not?","xpath! makes assertions, requiring truthy answers:","on.dom(\"xpath! name(/*) = 'nope'\") => undefined."]},{"id":38,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({}) => {} (fairly useless, but minimal, test case)."]},{"id":39,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ h:\"css head\", H:\"css html\" }) => { h:head, H:html }."]},{"id":40,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ h:\"css head\", f:\"css? foot\" }) => { h:head, f:null }."]},{"id":41,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ h:\"css head\", f:\"css foot\" }) => undefined (no foot!)."]},{"id":42,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ x:\"css* frame\" }) => { x:[] } (frames optional here)."]},{"id":43,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ x:\"css+ frame\" }) => undefined (but mandatory here!)."]},{"id":44,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ x:\"css* script\" }) => { x:[…all (>=0) script tags…] }."]},{"id":45,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ x:\"css+ script\" }) => { x:[…all (>0) script tags…] }."]},{"id":46,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ c:\"xpath count(//script)\" }) => {c:N} (any N is okay)."]},{"id":47,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ c:\"xpath! count(//script)\" }) => {c:N} (only N!=0 ok)."]},{"id":48,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ c:\"xpath! count(//missing)\" }) => undefined (as N==0)."]},{"id":49,"name":["on.dom(dom spec type 2: an object showing the structure you want)","on.dom({ c:\"xpath! count(//*) and /html\" }) => { c:true }."]},{"id":50,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([\"css* script[src]\", \"xpath string(@src)\"]) => [\"url\"…]."]},{"id":51,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([\"css? script:not([src])\", \"xpath string(.)\"]) => \"js…\"."]},{"id":52,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([\"css? script:not([src])\", \"xpath! string(@src)\"]) => undefined (empty string is not truthy => not a match)."]},{"id":53,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([\"xpath /svg\", \"css* *\"]) => undefined (not an svg doc)."]},{"id":54,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([html, \"xpath .\"]) => html."]},{"id":55,"name":["on.dom(dom spec type 3: [context_spec, per_match_spec])","on.dom([[head, html], \"xpath .\"]) => [head, html]."]},{"id":56,"name":["on.dom plugins:","on( { dom: \"my_plugin\", ready: ready = (x) -> } , { dom: \"my_plugin\": -> document.body } ) => ready(document.body)."]},{"id":57,"name":["on.dom plugins:","on.dom([\"my_plugin\", \"xpath .\"]) => body."]},{"id":58,"name":["on.dom plugins:","on.dom([\"my_plugin\", \"xpath ..\"]) => html."]},{"id":59,"name":["on.dom plugins:","on.dom(\"xpath .\") => document."]},{"id":60,"name":["on.query","should be a function after the first on() call."]},{"id":61,"name":["on.query","on.query() => {} for a missing query string."]},{"id":62,"name":["on.query","on.query() => {} for an empty query string (\"?\")."]},{"id":63,"name":["on.query","on.query() => { a:\"\", x:\"0\" } for a query string \"?a=&x=0\"."]},{"id":64,"name":["on.query","on.query() => { ugh:undefined } for a query string \"?ugh\"."]}]
,"results":[[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[{"id":17,"message":"Expected [ HTMLNode ] to equal [ HTMLNode, 1 ].","stacktrace":" Error: Expected [ HTMLNode ] to equal [ HTMLNode, 1 ].\n at new jasmine.ExpectationResult (http://localhost:7357/testem/jasmine.js:102:32)\n at null.toEqual (http://localhost:7357/testem/jasmine.js:1194:29)\n at null.<anonymous> (http://localhost:7357/tests/on.dom.js:113:46)\n at jasmine.Block.execute (http://localhost:7357/testem/jasmine.js:1024:15)\n at jasmine.Queue.next_ (http://localhost:7357/testem/jasmine.js:2025:31)\n at jasmine.Queue.start (http://localhost:7357/testem/jasmine.js:1978:8)\n at jasmine.Spec.execute (http://localhost:7357/testem/jasmine.js:2305:14)\n at jasmine.Queue.next_ (http://localhost:7357/testem/jasmine.js:2025:31)\n at jasmine.Queue.start (http://localhost:7357/testem/jasmine.js:1978:8)\n at jasmine.Suite.execute (http://localhost:7357/testem/jasmine.js:2450:14)\n ...\n"},{"id":81,"message":"Expected [ HTMLNode ] to equal [ HTMLNode, 1 ].","stacktrace":" ([object Object])@http://localhost:7357/testem/jasmine.js:102\n ([object Array])@http://localhost:7357/testem/jasmine.js:1199\n ()@http://localhost:7357/tests/on.dom.js:113\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:1024\n ()@http://localhost:7357/testem/jasmine.js:2025\n ((function () {spec.finish(onComplete);}))@http://localhost:7357/testem/jasmine.js:1978\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2305\n ()@http://localhost:7357/testem/jasmine.js:2025\n ((function () {self.finish(onComplete);}))@http://localhost:7357/testem/jasmine.js:1978\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2450\n ()@http://localhost:7357/testem/jasmine.js:2025\n ((function () {self.finish(onComplete);}))@http://localhost:7357/testem/jasmine.js:1978\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2450\n ()@http://localhost:7357/testem/jasmine.js:2025\n ((function () {self.finish(onComplete);}))@http://localhost:7357/testem/jasmine.js:1978\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2450\n ()@http://localhost:7357/testem/jasmine.js:2025\n ()@http://localhost:7357/testem/jasmine.js:2021\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2407\n ()@http://localhost:7357/testem/jasmine.js:2451\n ()@http://localhost:7357/testem/jasmine.js:2035\n ()@http://localhost:7357/testem/jasmine.js:2021\n ((function () {if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {completedSynchronously = true;return;}if (self.blocks[self.index].abort) {self.abort = true;}self.offset = 0;self.index++;var now = (new Date).getTime();if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {self.env.lastUpdate = now;self.env.setTimeout(function () {self.next_();}, 0);} else {if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {goAgain = true;} else {self.next_();}}}))@http://localhost:7357/testem/jasmine.js:2279\n ()@http://localhost:7357/testem/jasmine.js:2306\n ()@http://localhost:7357/testem/jasmine.js:2035\n ()@http://localhost:7357/testem/jasmine.js:2015\n ...\n"},{"id":145,"message":"Expected [ HTMLNode ] to equal [ HTMLNode, 1 ]."},{"id":209,"message":"Expected [ HTMLNode ] to equal [ HTMLNode, 1 ].","stacktrace":" test_dom([arguments not available])@http://localhost:7357/on.js:376\n <anonymous function>([arguments not available])@http://localhost:7357/tests/on.dom.js:59\n <anonymous function: jasmine.Block.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:1024\n <anonymous function: jasmine.Queue.prototype.next_>([arguments not available])@http://localhost:7357/testem/jasmine.js:2025\n <anonymous function: jasmine.Queue.prototype.start>([arguments not available])@http://localhost:7357/testem/jasmine.js:1978\n <anonymous function: jasmine.Spec.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:2305\n <anonymous function: jasmine.Queue.prototype.next_>([arguments not available])@http://localhost:7357/testem/jasmine.js:2025\n <anonymous function: jasmine.Queue.prototype.start>([arguments not available])@http://localhost:7357/testem/jasmine.js:1978\n <anonymous function: jasmine.Suite.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:2450\n <anonymous function: jasmine.Queue.prototype.next_>([arguments not available])@http://localhost:7357/testem/jasmine.js:2025\n \n Error created at <anonymous function: jasmine.ExpectationResult>([arguments not available])@http://localhost:7357/testem/jasmine.js:102\n <anonymous function: jasmine.Matchers.matcherFn_>([arguments not available])@http://localhost:7357/testem/jasmine.js:1194\n <anonymous function>([arguments not available])@http://localhost:7357/tests/on.dom.js:113\n <anonymous function: jasmine.Block.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:1024\n <anonymous function: jasmine.Queue.prototype.next_>([arguments not available])@http://localhost:7357/testem/jasmine.js:2025\n <anonymous function: jasmine.Queue.prototype.start>([arguments not available])@http://localhost:7357/testem/jasmine.js:1978\n <anonymous function: jasmine.Spec.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:2305\n <anonymous function: jasmine.Queue.prototype.next_>([arguments not available])@http://localhost:7357/testem/jasmine.js:2025\n <anonymous function: jasmine.Queue.prototype.start>([arguments not available])@http://localhost:7357/testem/jasmine.js:1978\n <anonymous function: jasmine.Suite.prototype.execute>([arguments not available])@http://localhost:7357/testem/jasmine.js:2450\n ...\n"},{"id":273,"message":"Expected [ HTMLNode ] to equal [ HTMLNode, 1 ]."}],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
}
var table = d3.select('#chart');
// { environments: [ { name, date, version?, percent, scores, maxscore } ]
// , tests: [ { id, name[], url? } ]
// , results: [[ {status: pass|fail, column: #, message?, stacktrace?, raw?} ]]
// }
d3.json('tap.json', function(json) {
window.json = json;
function score(browser, i) {
var res = json.results
, max = res.length
, n = res.filter(function(t) { return t[i].status == 'pass'; }).length;
browser.percent = 100 * (n / max) + '%';
browser.maxscore = max;
browser.score = n;
return browser;
}
// d3.selectAll('#chart thead th').style('background-image',function(d,i){console.log(i);if(!d)return;return '-webkit-linear-gradient(bottom, #'+(i&1?'f0f0f0':'fff')+' '+d.percent+',rgba(255, 0, 0, 0.3) '+d.percent+')';})
// expand the "0" short form to { status: 'pass' } (and set the column # info)
var results = json.results.map(function(row) {
return row.map(function(d,i) {
if (d) d.status = 'fail';
else d = { status:'pass' };
d.column = i + 1;
return d;
});
})
/* [ { key: "Firefox"
, values: [ { its different versions } ]
}
, ... the other renderers
] */
, browsers = d3.nest()
.key(function(d) { return d.name.toLowerCase(); })
.entries(json.environments = json.environments.map(score))
, colgroups = [{ key: "test-names", values:[] }].concat(browsers)
;
json.results = results; // ease debugging
table.attr('cols', 1 + json.environments.length);
// grouping together all shown browsers by browser family
var cg = table.selectAll('colgroup')
.data(colgroups);
cg.enter().append('colgroup')
.attr('class', function(d) { return d.key; })
//.attr('span', function(d) { return d.values.length || undefined; })
.selectAll('col')
.data(function(d) { return d.values; })
.enter().append('col')
.attr('title', function(d) { return d.date; })
.text(function(d) { return d.version; }) // NOP, when no version given
;
var thead = table.select('thead')
, tr = thead.select('tr')
, legend = thead.select('th');
table.node().appendChild(thead.node()); // needs to be after the <colgroups>
// the browser name headers
var ths = window.ths = thead.select('tr').selectAll('th')
.data([null].concat(json.environments))
.enter().append('th')
.attr('class', function(d) { return d && d.type; }) // NOP
;
// name + date
ths.append('div').text(function(d, i) { return d && d.name + ' '; })
.append('span').text(function(d) { return d && d.date; })
;
tr.node().appendChild(legend.node()); // put our legend at the far right
function hovered_header(d) {
var col = d3.event.target.__data__.column, th;
if ((th = ths[0][col]))
if ('classList' in th) th.classList.add('hover');
else d3.select(th).classed('hover', true);
}
function leaving_header(d) {
var col = d3.event.target.__data__.column, th;
if ((th = ths[0][col]))
if ('classList' in th) th.classList.remove('hover');
else d3.select(th).classed('hover', false);
}
// all table rows on the main grid, and the default hover title for that line
tr = window.tr = table
.append('tbody').attr('class', 'data')
.on('mouseover', hovered_header)
.on('mouseout', leaving_header)
.selectAll('tr').data(json.tests)
.enter().append('tr')
.attr('title', function(d) { return d.name.join(' -> '); })
;
// grid columns: data from the tests
tr.selectAll('td')
.data(function(d, i) { return results[i]; })
.enter().append('td')
.attr('class', function(d) { return d.status; })
.attr('title', function(d) { return d.message; })
//.classed('died', function(d) { return d.crash; })
;
// last column: the test id and name
var group;
tr.append('th')
.classed('group', function(d) {
var my_group = d.name.slice(0, 1).join('\t') // FIXME: 1 -> -1, perhaps?
, was_same = my_group === group;
group = my_group;
return !was_same;
})
.append('a')
.style('margin-left', function(d) { return (d.name.length - 1)*5 + 'px';})
.attr('name', function(d) { return 'test-'+ d.id; })
.attr('href', function(d) { return d && d.url || '#test-'+ d.id; })
.text(function(d, i) { return d.name[d.name.length - 1]; })
.attr('title', function(d, i) { return d.name.slice(0,-1).join(' -> '); })
;
table.selectAll('td.pass, td.warn, td.fail').on('click', function(d) {
if (d.message) {
var msg = 'Message: '+ d.message;
if (d.stacktrace)
msg += '\n\nstack trace:\n'+ d.stacktrace;
alert(msg);
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment