├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── dom_consumer.js └── tape_css.js ├── tape-dom.png ├── tape.css └── test ├── index.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | test/test-dist.js 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Victor Grishchenko, Citrea LLC 2 | Copyright (c) 2012-2014 Aleksei Balandin, Citrea LLC 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM output formatter for tape tests 2 | 3 | Convert tape's TAP output to nicely formatted DOM. 4 | 5 | ![UI](https://raw.githubusercontent.com/gritzko/tape-dom/master/tape-dom.png) 6 | 7 | Tape is an [NPM package](https://www.npmjs.com/package/tape) for 8 | making [Test Anything Protocol](https://testanything.org/) tests in node.js. 9 | Tape nicely runs in a browser using browserify or in a browser-based 10 | debugger like [IronNode](https://github.com/s-a/iron-node). 11 | 12 | Isomorphic use: 13 | 14 | var tape = require('tape'); 15 | // If DOM tree is available (browser, IronNode) then render 16 | // results to DOM. Otherwise, do nothing. 17 | require('tape-dom')(tape); 18 | 19 | Then `browserify my_js_test.js -o browserified_test.js` 20 | 21 | The HTML side: 22 | 23 | 24 | 25 | tape-dom example 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tape-dom", 3 | "version": "0.0.12", 4 | "homepage": "http://github.com/gritzko/tape-dom", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/gritzko/tape-dom.git" 8 | }, 9 | "author": { 10 | "email": "victor.grishchenko@gmail.com", 11 | "name": "Victor Grishchenko" 12 | }, 13 | "email": "swarm.js@gmail.com", 14 | "license": "MIT", 15 | "files": [ 16 | "src/*.js", 17 | "test/test.js", 18 | "test/index.html", 19 | "LICENSE", 20 | "README.md", 21 | "tape.css" 22 | ], 23 | "main": "src/dom_consumer.js", 24 | "browser": "src/dom_consumer.js", 25 | "dependencies": { 26 | "googlediff": "^0.1.0", 27 | "tap-parser": "^1.1.6" 28 | }, 29 | "devDependencies": { 30 | "browserify": "13.1.0", 31 | "tape": "^4.0.2", 32 | "tape-dom": "^0.0.4" 33 | }, 34 | "scripts": { 35 | "pretest": "browserify test/test.js -o test/test-dist.js", 36 | "test": "open test/index.html" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/dom_consumer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var DiffMatchPatch = require('googlediff'); 3 | var dmp = new DiffMatchPatch(); 4 | //var tape = require('tape'); 5 | var tape_css = require('./tape_css'); 6 | 7 | 8 | function html_diff (a, b) { 9 | var diff = dmp.diff_main(b, a); 10 | dmp.diff_cleanupSemantic(diff); 11 | var ret = '', tag; 12 | diff.forEach(function(chunk){ 13 | switch (chunk[0]) { 14 | case 0: tag = 'span'; break; 15 | case 1: tag = 'ins'; break; 16 | case -1: tag = 'del'; break; 17 | } 18 | ret += '<'+tag+'>' + chunk[1] + ''; 19 | }); 20 | return ret; 21 | } 22 | 23 | 24 | function startTestDiv (row) { 25 | var test_div = document.createElement('DIV'); 26 | test_div.id = row.id; 27 | test_div.setAttribute('class', 'test'); 28 | 29 | var name = document.createElement('P'); 30 | name.setAttribute('class', 'name'); 31 | name.appendChild(document.createTextNode(row.name)); 32 | test_div.appendChild(name); 33 | 34 | current_test = test_div; 35 | test_root.appendChild(test_div); 36 | 37 | return test_div; 38 | } 39 | 40 | 41 | function assertDiv (row, root) { 42 | var p = document.createElement('P'); 43 | p.setAttribute('class', 'assert '+(row.ok?'ok':'fail')); 44 | 45 | var ok = document.createElement('SPAN'); 46 | ok.setAttribute('class', 'ok'); 47 | ok.appendChild(document.createTextNode(row.ok ? 'OK' : 'FAIL')); 48 | p.appendChild(ok); 49 | 50 | if (row.name) { 51 | var nam = document.createElement('SPAN'); 52 | nam.setAttribute('class', 'name'); 53 | nam.appendChild(document.createTextNode(row.name)); 54 | p.appendChild(nam); 55 | } 56 | root.appendChild(p); 57 | p.scrollIntoView({block: "end", behavior: "smooth"}); 58 | return p; 59 | } 60 | 61 | function commentDiv (row, root) { 62 | var p = document.createElement('P'); 63 | p.setAttribute('class', 'comment'); 64 | p.appendChild(document.createTextNode(row)); 65 | root.appendChild(p); 66 | return p; 67 | } 68 | 69 | function endDiv(row, current_test) { 70 | var p = document.createElement('P'); 71 | p.setAttribute('class', 'end'); 72 | current_test.appendChild(p); 73 | return p; 74 | } 75 | 76 | function assertFailDiv (row, assert_element) { 77 | var actual = row.actual; 78 | var expected = row.expected; 79 | if (actual && expected && 80 | typeof(actual)=='object' && 81 | typeof(expected)=='object') { 82 | actual = JSON.stringify(actual); 83 | expected = JSON.stringify(expected); 84 | } 85 | 86 | var actual_span = document.createElement('SPAN'); 87 | actual_span.setAttribute('class', 'actual'); 88 | actual_span.appendChild(document.createTextNode(actual)); 89 | assert_element.appendChild(actual_span); 90 | 91 | var expected_span = document.createElement('SPAN'); 92 | expected_span.setAttribute('class', 'expected'); 93 | expected_span.appendChild(document.createTextNode(expected)); 94 | assert_element.appendChild(expected_span); 95 | 96 | if (row.file) { 97 | var line = document.createElement('SPAN'); 98 | line.setAttribute('class', 'line'); 99 | var m = /\/([^\/]+)$/.exec(row.file); 100 | var file_line = m[1]; 101 | line.appendChild(document.createTextNode(file_line)); 102 | assert_element.appendChild(line); 103 | // this way the user may meaningfully navigate the code 104 | console.warn(row.error.stack); 105 | } 106 | 107 | if (actual && expected && 108 | actual.constructor==String && 109 | expected.constructor==String) 110 | { 111 | var diff = document.createElement('P'); 112 | diff.setAttribute('class', 'diff'); 113 | diff.innerHTML = html_diff(actual, expected); 114 | assert_element.appendChild(diff); 115 | } 116 | 117 | } 118 | 119 | var test_root, current_test; 120 | if (typeof(document)==='object') { 121 | test_root = document.getElementById('tests'); 122 | if (!test_root) { 123 | test_root = document.createElement('div'); 124 | test_root.setAttribute('id','tests'); 125 | document.body.appendChild(test_root); 126 | } 127 | current_test = test_root; 128 | } 129 | 130 | function add_some_dom (row) { 131 | if (row.type==='test') { 132 | current_test = startTestDiv(row, current_test); 133 | } else if (row.type==='assert') { 134 | var assert_element = assertDiv(row, current_test); 135 | 136 | if (!row.ok) { 137 | assertFailDiv(row, assert_element); 138 | } 139 | } else if (row.type==='end') { 140 | endDiv(row, current_test); 141 | current_test = current_test.parentNode; 142 | } else if (row.constructor===String) { 143 | commentDiv(row, current_test); 144 | } else { 145 | console.warn('tape-dom row', row.type, row); 146 | } 147 | } 148 | 149 | function stream (tape) { 150 | var stream = tape.createStream({ objectMode: true }); 151 | stream.on('data', add_some_dom); 152 | } 153 | 154 | function installCSS () { 155 | var link = document.createElement('style'); 156 | link.setAttribute("type", "text/css"); 157 | var css_body = document.createTextNode(tape_css); 158 | link.appendChild(css_body); 159 | document.head.appendChild(link); 160 | } 161 | 162 | function init (tape) { 163 | if (typeof(window)==='object') { 164 | installCSS(); 165 | stream(tape); 166 | } 167 | return init; 168 | } 169 | 170 | init.installCSS = installCSS; 171 | init.stream = stream; 172 | 173 | module.exports = init; 174 | -------------------------------------------------------------------------------- /src/tape_css.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = 3 | "div.test{width:100%;border-top:1px dotted grey;margin-top:1em;background:#eef;font-weight:700;margin-bottom:1em}"+ 4 | "div#tests{margin-bottom:5em}"+ 5 | "p.assert{margin-top:0;margin-bottom:0;font-weight:400;width:100%}"+ 6 | "p.assert.ok{background:#efe;color:grey}"+ 7 | "p.assert.fail{background:#faa}"+ 8 | "p.comment{background:#eef;color:grey;font-style:italic;margin-top:.2em;margin-bottom:.2em;padding-left:2.4em}"+ 9 | "p.end{border-bottom:1px dotted grey;margin-top:1em;background:#eef;margin-top:.2em;margin-bottom:.2em}"+ 10 | "p.diff{background:#ffe;margin-top:0;margin-bottom:0;padding-top:.25em;padding-bottom:.25em;padding-left:4em;font-weight:400;white-space:pre-wrap}"+ 11 | "ins{background:#afa}"+ 12 | "del{background:#faa;text-decoration:strike}"+ 13 | "span.ok{font-weight:700}"+ 14 | "span.actual,span.expected,span.name{padding-left:1em;font-style:italic;max-width:25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}"+ 15 | "p.assert.fail span.name{color:#555}"+ 16 | "span.expected:before{content:'!=';padding-right:1em}"+ 17 | "span.line{padding-left:1em;float:right;text-align:right;overflow:hidden;text-overflow:ellipsis;font-family:monospace;color:#888}" 18 | ; 19 | -------------------------------------------------------------------------------- /tape-dom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gritzko/tape-dom/700774416fae6d18f4639ca3251e05942fb3838e/tape-dom.png -------------------------------------------------------------------------------- /tape.css: -------------------------------------------------------------------------------- 1 | div.test { 2 | width: 100%; 3 | border-top: 1px dotted grey; 4 | margin-top: 1em; 5 | background: #eef; 6 | font-weight: bold; 7 | } 8 | p.assert { 9 | margin-top: 0px; 10 | margin-bottom: 0px; 11 | font-weight: normal; 12 | width: 100%; 13 | } 14 | p.assert.ok { 15 | background: #efe; 16 | color: grey; 17 | } 18 | p.assert.fail { 19 | background: #faa; 20 | } 21 | p.comment { 22 | background: #eef; 23 | color: grey; 24 | font-style: italic; 25 | margin-top: 0.2em; 26 | margin-bottom: 0.2em; 27 | padding-left: 2.4em; 28 | } 29 | p.end { 30 | border-bottom: 1px dotted grey; 31 | margin-top: 1em; 32 | background: #eef; 33 | margin-top: 0.2em; 34 | margin-bottom: 0.2em; 35 | } 36 | p.diff { 37 | background: #ffe; 38 | margin-top: 0px; 39 | margin-bottom: 0px; 40 | padding-top: 0.25em; 41 | padding-bottom: 0.25em; 42 | padding-left: 4em; 43 | font-weight: normal; 44 | white-space: pre-wrap; 45 | } 46 | ins { 47 | background: #afa; 48 | } 49 | del { 50 | background: #faa; 51 | text-decoration: strike; 52 | } 53 | span.ok { 54 | font-weight: bold; 55 | } 56 | span.actual, span.expected, span.name { 57 | padding-left: 1em; 58 | font-style: italic; 59 | max-width: 25em; 60 | overflow: hidden; 61 | text-overflow: ellipsis; 62 | white-space: nowrap; 63 | display: inline-block; 64 | } 65 | p.assert.fail span.name { 66 | color: #555; 67 | } 68 | span.expected:before { 69 | content: '!='; 70 | padding-right: 1em; 71 | } 72 | span.line { 73 | padding-left: 1em; 74 | float: right; 75 | text-align: right; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | font-family: monospace; 79 | color: #888; 80 | } 81 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tape-dom tests 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tape = require('tape'); 4 | if (typeof(window)==='object') { 5 | var tape_dom = require('..'); 6 | tape_dom.installCSS(); 7 | tape_dom.stream(tape); 8 | // This also works 9 | // require('..')(tape) 10 | } 11 | 12 | 13 | tape('trivial matches', function perfect_matches (tap) { 14 | tap.plan(5); 15 | tap.equal(5,5); 16 | tap.equal("string","string"); 17 | tap.deepEqual({x:1, y:{z:3}}, {x:1, y:{z:3}}); 18 | tap.ok(true); 19 | tap.throws(function evil(){ 20 | throw new Error("i vil kil you"); 21 | }, /kil/, "it throws"); 22 | tap.comment("all those are OK"); 23 | }); 24 | 25 | tape('simple mismatches', function complete_mismatches (tap) { 26 | tap.plan(4); 27 | tap.equal(5,6, 'comparing 5 to 6'); 28 | tap.equal("string",""); 29 | tap.deepEqual({x:1, y:{z:3}}, {x:1, y:{z:2}}); 30 | tap.ok(false); 31 | tap.throws(function evil(){ 32 | throw new Error("i vil mis you"); 33 | }, /kil/, "it throws"); 34 | tap.comment("all those are not OK"); 35 | }); 36 | --------------------------------------------------------------------------------