├── .eslintignore ├── docs ├── example.png └── index.html ├── test ├── fixtures │ ├── empty.txt │ ├── test-with-plan-as-range.txt │ ├── flat.txt │ └── nested.txt └── index.js ├── lib ├── style.css └── generate.js ├── .tryitout ├── package.json ├── .gitignore ├── bin └── index.js ├── README.md ├── CHANGELOG.md └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs 4 | -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielcsapo/tap-html/HEAD/docs/example.png -------------------------------------------------------------------------------- /test/fixtures/empty.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # test this 3 | 4 | 1..0 5 | # tests 0 6 | # pass 0 7 | 8 | # ok 9 | -------------------------------------------------------------------------------- /test/fixtures/test-with-plan-as-range.txt: -------------------------------------------------------------------------------- 1 | 1..2 2 | # Some description of a test 3 | ok 1 Yep a test description 4 | # Some other description of a test 5 | ok 2 And this one is another test description 6 | -------------------------------------------------------------------------------- /test/fixtures/flat.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # serial connection 3 | ok 1 got serialport 4 | # ble 5 | ok 2 got device discover 6 | ok 3 device connected 7 | ok 4 got characteristic 8 | ok 5 subcribed 9 | ok 6 got round-trip message 10 | 11 | 1..6 12 | # tests 6 13 | # pass 6 14 | 15 | # ok 16 | -------------------------------------------------------------------------------- /test/fixtures/nested.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # json-ex 3 | # should be able to stringify a basic javascript object 4 | ok 1 should be equivalent 5 | # should be able to stringify a complex javascript object 6 | ok 2 should be equal 7 | # should be able to parse a complex object 8 | ok 3 should be equal 9 | ok 4 should be equal 10 | ok 5 should be equal 11 | ok 6 should be equal 12 | # should be able to stringify a complex javascript object and parse it back 13 | ok 7 should be equal 14 | ok 8 should be equal 15 | ok 9 should be equal 16 | ok 10 should be equal 17 | ok 11 should be equal 18 | ok 12 should be equal 19 | ok 13 should be equal 20 | 21 | 1..13 22 | # tests 13 23 | # pass 13 24 | 25 | # ok 26 | -------------------------------------------------------------------------------- /lib/style.css: -------------------------------------------------------------------------------- 1 | /* Overrides */ 2 | 3 | html, body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Open Sans', Helvetica, sans-serif; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | h1, h3 { 12 | margin-bottom: 0px; 13 | margin-top: 0px; 14 | } 15 | 16 | .navbar { 17 | position: fixed; 18 | top: 0; 19 | } 20 | 21 | .navbar-title { 22 | margin-top: 25px !important; 23 | } 24 | 25 | #root { 26 | padding-bottom: 60px; 27 | } 28 | 29 | .list, .list-item { 30 | border-radius: 0 !important; 31 | border-right: 0 !important; 32 | border-left: 0 !important; 33 | } 34 | 35 | .badge { 36 | height: 25px; 37 | width: 25px; 38 | font-size: 10px; 39 | padding: 1px !important; 40 | display: inline-block !important; 41 | line-height: 25px; 42 | } 43 | -------------------------------------------------------------------------------- /.tryitout: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'tap-html', 3 | nav: { 4 | Source: 'https://github.com/gabrielcsapo/tap-html', 5 | Example: './example/index.html' 6 | }, 7 | body: ` 8 |
9 |

tap-html

10 |
📊 an html tap reporter

11 |
tape test | tap-html --out report.html
12 |
13 | 14 |
15 |
16 | `, 17 | template: 'landing', 18 | options: { 19 | width: '100%' 20 | }, 21 | output: './docs', 22 | footer: ` 23 |
Made with ☕️ by @gabrielcsapo
24 | ` 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tap-html", 3 | "version": "1.1.0", 4 | "description": "📊 an html tap reporter", 5 | "author": "Gabriel J. Csapo ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/gabrielcsapo/tap-html.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/gabrielcsapo/tap-html/issues" 13 | }, 14 | "homepage": "https://github.com/gabrielcsapo/tap-html#readme", 15 | "main": "index.js", 16 | "scripts": { 17 | "lint": "semistandard", 18 | "test": "tape test/index.js | ./bin/index.js --out ./docs/example/index.html", 19 | "coverage": "tap test --coverage --coverage-report=lcov", 20 | "generate-docs": "tryitout" 21 | }, 22 | "bin": { 23 | "tap-html": "./bin/index.js" 24 | }, 25 | "dependencies": { 26 | "duplexer": "^0.1.1", 27 | "psychic-ui": "^1.0.7", 28 | "tap-parser": "^12.0.1", 29 | "through2": "^4.0.2" 30 | }, 31 | "devDependencies": { 32 | "semistandard": "^16.0.1", 33 | "tap": "^16.3.4", 34 | "tape": "^5.6.3", 35 | "tryitout": "^2.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | package-lock.json 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | .DS_Store 61 | tap-html.html 62 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const parser = require('../'); 7 | const { generate } = require('../lib/generate'); 8 | 9 | let program = {}; 10 | let exitCode; 11 | const args = process.argv.slice(2); 12 | 13 | args.forEach((arg, i) => { 14 | switch (arg) { 15 | case '-v': 16 | case '--version': 17 | case 'version': 18 | console.log(`v${require('../package.json').version}`); // eslint-disable-line 19 | exitCode = 0; 20 | break; 21 | case '-h': 22 | case '--help': 23 | case 'help': 24 | console.log(`` + // eslint-disable-line 25 | ` 26 | Usage: tap-html [options] 27 | 28 | Commands: 29 | -h, --help, help Output usage information 30 | -v, --version, version Output the version number 31 | 32 | Options: 33 | -o, --outFile [path] If instead of piping content you want it to be written to an html file locally please specify the relative path 34 | `); 35 | exitCode = 0; 36 | break; 37 | case '-o': 38 | case '--out': 39 | program['out'] = path.resolve(process.cwd(), args[i + 1]); 40 | break; 41 | } 42 | }); 43 | 44 | if (exitCode >= 0) process.exit(exitCode); 45 | 46 | const { out } = program; 47 | 48 | process.stdin 49 | .pipe(parser((res) => { 50 | // generate the html report 51 | const output = generate(res); 52 | 53 | // Produce output either in a file or on stdout. 54 | if (out) { 55 | const outputPath = path.resolve(__dirname, out); 56 | fs.writeFileSync(outputPath, output); 57 | } else { 58 | process.stdout.write(output); 59 | } 60 | })) 61 | .pipe(process.stdout); 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tap-html 2 | 3 | > 📊 an html tap reporter 4 | 5 | [![Npm Version](https://img.shields.io/npm/v/tap-html.svg)](https://www.npmjs.com/package/tap-html) 6 | [![Build Status](https://travis-ci.org/gabrielcsapo/tap-html.svg?branch=master)](https://travis-ci.org/gabrielcsapo/tap-html) 7 | [![Coverage Status](https://lcov-server.gabrielcsapo.com/badge/github%2Ecom/gabrielcsapo/tap-html.svg)](https://lcov-server.gabrielcsapo.com/coverage/github%2Ecom/gabrielcsapo/tap-html) 8 | [![Dependency Status](https://starbuck.gabrielcsapo.com/badge/github/gabrielcsapo/tap-html/status.svg)](https://starbuck.gabrielcsapo.com/github/gabrielcsapo/tap-html) 9 | [![devDependency Status](https://starbuck.gabrielcsapo.com/badge/github/gabrielcsapo/tap-html/dev-status.svg)](https://starbuck.gabrielcsapo.com/github/gabrielcsapo/tap-html#info=devDependencies) 10 | [![npm](https://img.shields.io/npm/dt/tap-html.svg)]() 11 | [![npm](https://img.shields.io/npm/dm/tap-html.svg)]() 12 | 13 | ## Installation 14 | 15 | ``` 16 | npm install tap-html --save-dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | ``` 22 | Usage: tap-html [options] 23 | 24 | Commands: 25 | -h, --help, help Output usage information 26 | -v, --version, version Output the version number 27 | 28 | Options: 29 | -o, --outFile [path] If instead of piping content you want it to be written to an html file locally please specify the relative path 30 | ``` 31 | 32 | By default tap parser should be pipeable as such: 33 | 34 | ``` 35 | tape test/**.js | tap-html | html-cleanup > index.html 36 | ``` 37 | 38 | To generate a report running the following will create a file for you. 39 | 40 | > This will generate a tap-html.html file 41 | 42 | ``` 43 | tape test/**.js | tap-html --out ./tap-html.html 44 | ``` 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 (03/14/2023) 2 | 3 | - fixes issue when no assert name is provided (#11) - @achappuis 4 | - fixes issue when no test name is provided with TAP comment (#11) - @achappuis 5 | - Change behavior when no output file is given to tap-html to restore what is described in README.md (#5) - @achappuis 6 | - Updates deps to latest - @gcsapo 7 | 8 | # 1.0.1 (04/20/2019) 9 | 10 | - fixes backwards compatibility with node@8 11 | 12 | # 1.0.0 (04/20/2019) 13 | 14 | - removes react dependencies 15 | - inlines assets 16 | 17 | # 0.3.0 (03/04/2019) 18 | 19 | - fixes issue with plan not checking for plan name to be a string and instead uses range 20 | 21 | # 0.2.1 (05/09/2018) 22 | 23 | - includes `babel-minify-webpack-plugin` as a dependency instead of devDependency. 24 | 25 | # 0.2.0 (12/20/2017) 26 | 27 | - removes commander 28 | - improves readability 29 | - adds a named function `tapHTML` for readability 30 | - updates dependencies 31 | 32 | # 0.1.8 (11/13/2017) 33 | 34 | - uses preact to reduce bundle size. 35 | - 208 KB -> 130 KB (60% reduction in size) 36 | 37 | # 0.1.7 (10/18/2017) 38 | 39 | - uses babel-preset-env instead of babel-preset-es2015 40 | - updates webpack and babel 41 | - uses babel-minify-webpack-plugin to minify es6 42 | 43 | # 0.1.6 (10/11/2017) 44 | 45 | - fixes a styling issue when trying to display an assertion without a parent as the first item in the list 46 | 47 | # 0.1.5 (10/11/2017) 48 | 49 | - moves font-awesome to dependencies 50 | 51 | # 0.1.4 (10/11/2017) 52 | 53 | - updates dependencies 54 | 55 | # 0.1.3 (09/28/2017) 56 | 57 | - updates to react@16.0.0 58 | 59 | # 0.1.2 (09/17/2017) 60 | 61 | - will recursively generate output directory if it doesn't exist 62 | - bundles font-awesome with output 63 | - adds storybook 64 | 65 | # 0.1.1 (09/13/2017) 66 | 67 | - fixes error when data is undefined and the extra event is triggered 68 | 69 | # 0.1.0 (09/11/2017) 70 | 71 | - adds fields to `package.json` that were formerly missing 72 | - fixes a bug with flat tests 73 | - adds tests for flat and nested tests for the parse functionality 74 | - fixes bug with empty test suites 75 | - default behavior will output to stdout, an added flag --out will write to disk 76 | 77 | # 0.0.5 (07/16/2017) 78 | 79 | - updates and fixes implementation with tap-parser@6.0.0 80 | 81 | # 0.0.4 (06/27/2017) 82 | 83 | - fixes issue with webpack ignoring node_modules folder, fixes it by only including `tap-html` 84 | 85 | # 0.0.3 (06/27/2017) 86 | 87 | - upgrades `webpack@^2.6.1` -> `webpack@^3.0.0` 88 | - upgrades `babel-loader@^7.0.0` -> `babel-loader@^7.1.1` 89 | - upgrades `html-webpack-plugin@^2.28.0` -> `html-webpack-plugin@^2.29.0` 90 | - upgrades `react@^15.5.4` -> `react@^15.6.1` 91 | - upgrades `react-dom@^15.5.4` -> `react-dom@^15.6.1` 92 | - fixes the bin name to be the correct one 93 | 94 | # 0.0.2 (06/13/2017) 95 | 96 | - removes console statements and unused code paths 97 | 98 | # 0.0.1 (06/07/2017) 99 | 100 | - basic functionality 101 | 102 | # 0.0.0 103 | 104 | - keeps npm name 105 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Parser } = require("tap-parser"); 2 | const Through = require("through2"); 3 | const Duplexer = require("duplexer"); 4 | 5 | module.exports = function tapHTML(callback) { 6 | const tap = new Parser(); 7 | const out = Through.obj(); 8 | const dup = Duplexer(tap, out); 9 | 10 | let currentPlan = -1; 11 | let currentAssertion = -1; 12 | let data = []; 13 | let plan = null; 14 | 15 | const startTime = Date.now(); 16 | 17 | function pushTest(name) { 18 | data.push({ 19 | type: "test", 20 | name: name, 21 | start: Date.now(), 22 | assertions: [], 23 | }); 24 | 25 | // get the current index of the plan 26 | // so that we can use this to push the current assertions to it 27 | currentPlan += 1; 28 | currentAssertion = -1; 29 | } 30 | 31 | tap.on("comment", (res) => { 32 | if (!plan) { 33 | pushTest(res); 34 | } 35 | }); 36 | 37 | tap.on("plan", (res) => { 38 | if (typeof res !== "string") return; 39 | 40 | plan = res; 41 | }); 42 | 43 | tap.on("extra", (res) => { 44 | if (data && currentPlan > 0 && currentAssertion > 0) { 45 | data[currentPlan]["assertions"][currentAssertion][ 46 | "console" 47 | ] += `${res}\n`; 48 | } 49 | }); 50 | 51 | tap.on("assert", (res) => { 52 | // If no plan is registered yet, create a default plan. 53 | if (currentPlan == -1) { 54 | pushTest("default"); 55 | } 56 | 57 | // TAP does not require a name. If no name is registered, set a default name. 58 | if (!res.name) { 59 | res.name = "test #" + res.id; 60 | } 61 | 62 | data[currentPlan].assertions.push({ 63 | type: "assert", 64 | number: res.id, 65 | name: res.name, 66 | ok: res.ok, 67 | diag: res.diag, 68 | console: "", 69 | end: Date.now(), 70 | }); 71 | currentAssertion += 1; 72 | }); 73 | 74 | tap.on("complete", (res) => { 75 | res["time"] = Date.now() - startTime; 76 | 77 | var plan = -1; 78 | 79 | // combine and clean up tests 80 | for (var i = 0; i < data.length; i++) { 81 | // trims the name from having any extra new line breaks 82 | data[i].name = data[i].name.trim(); 83 | 84 | // This is a top level plan 85 | if (data[i].assertions.length === 0) { 86 | // move on with the tests 87 | plan = i; 88 | data[plan].tests = []; 89 | delete data[i].assertions; 90 | } else if (plan === -1) { 91 | // this is flat plan that has no parent do nothing 92 | } else { 93 | // We know this is part of the currentPlan 94 | if (!data[plan]) { 95 | data[plan] = { 96 | tests: [], 97 | }; 98 | } else { 99 | data[plan].tests = data[plan].tests || []; 100 | } 101 | 102 | data[plan].tests.push(data[i]); 103 | delete data[i]; 104 | } 105 | } 106 | 107 | data = data.filter((d) => d > ""); 108 | 109 | function calculateTime(test) { 110 | if (test.end) return; 111 | 112 | test.end = test.assertions[test.assertions.length - 1].end; 113 | test.assertions.forEach((assertion) => { 114 | assertion.start = test.start; 115 | }); 116 | } 117 | data.forEach((plan) => { 118 | if (plan.tests && plan.tests.length === 0) { 119 | // this is an empty test 120 | plan.end = plan.start; 121 | } else if (plan.tests && plan.tests.length > 0) { 122 | for (var i = 0; i < plan.tests.length; i++) { 123 | calculateTime(plan.tests[i]); 124 | } 125 | plan.end = plan.tests[plan.tests.length - 1].end; 126 | } else { 127 | // this is a flat test with only assertions 128 | calculateTime(plan); 129 | } 130 | }); 131 | 132 | res["tests"] = data; 133 | callback(res); 134 | }); 135 | 136 | return dup; 137 | }; 138 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require("tape"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | const parser = require("../index"); 6 | 7 | test("tap-html", (t) => { 8 | t.plan(4); 9 | 10 | t.test("should be able to parse test that starts with range", (t) => { 11 | const testWithPlanAsRange = fs.createReadStream( 12 | path.resolve(__dirname, "./fixtures/test-with-plan-as-range.txt") 13 | ); 14 | testWithPlanAsRange.pipe( 15 | parser((res) => { 16 | t.equal(res.ok, true); 17 | t.equal(res.count, 2); 18 | t.equal(res.pass, 2); 19 | t.equal(res.fail, 0); 20 | t.equal(res.bailout, false); 21 | t.equal(res.todo, 0); 22 | t.equal(res.skip, 0); 23 | console.dir(res.plan); 24 | t.equal(res.plan.start, 1); 25 | t.equal(res.plan.end, 2); 26 | t.equal(res.plan.skipAll, false); 27 | t.equal(res.plan.skipReason, ""); 28 | t.equal(res.plan.comment, ""); 29 | t.deepEqual(res.failures, []); 30 | t.equal(res.tests.length, 2); 31 | t.equal(res.tests[0].type, "test"); 32 | t.equal(res.tests[0].name, "# Some description of a test"); 33 | t.equal(res.tests[0].assertions.length, 1); 34 | t.equal(res.tests[0].assertions[0].type, "assert"); 35 | t.equal(res.tests[0].assertions[0].number, 1); 36 | t.equal(res.tests[0].assertions[0].name, "Yep a test description"); 37 | t.equal(res.tests[0].assertions[0].ok, true); 38 | t.equal(res.tests[0].assertions[0].console, ""); 39 | 40 | t.equal(res.tests[1].type, "test"); 41 | t.equal(res.tests[1].name, "# Some other description of a test"); 42 | t.equal(res.tests[1].assertions.length, 1); 43 | t.equal(res.tests[1].assertions[0].type, "assert"); 44 | t.equal(res.tests[1].assertions[0].number, 2); 45 | t.equal( 46 | res.tests[1].assertions[0].name, 47 | "And this one is another test description" 48 | ); 49 | t.equal(res.tests[1].assertions[0].ok, true); 50 | t.equal(res.tests[1].assertions[0].console, ""); 51 | t.end(); 52 | }) 53 | ); 54 | }); 55 | 56 | t.test("should be able to parse flat test", (t) => { 57 | const flat = fs.createReadStream( 58 | path.resolve(__dirname, "./fixtures/flat.txt") 59 | ); 60 | flat.pipe( 61 | parser((res) => { 62 | t.equal(res.ok, true); 63 | t.equal(res.count, 6); 64 | t.equal(res.pass, 6); 65 | t.equal(res.fail, 0); 66 | t.equal(res.bailout, false); 67 | t.equal(res.todo, 0); 68 | t.equal(res.skip, 0); 69 | t.ok(Array.isArray(res.failures)); 70 | t.equal(res.failures.length, 0); 71 | t.equal(typeof res.time, "number"); 72 | 73 | t.ok(Array.isArray(res.tests)); 74 | 75 | t.equal(res.tests[0].type, "test"); 76 | t.equal(res.tests[0].name, "# serial connection"); 77 | t.equal(typeof res.tests[0].start, "number"); 78 | t.equal(typeof res.tests[0].end, "number"); 79 | t.equal(res.tests[0].assertions.length, 1); 80 | 81 | t.equal(res.tests[0].assertions[0].type, "assert"); 82 | t.equal(res.tests[0].assertions[0].number, 1); 83 | t.equal(res.tests[0].assertions[0].diag, null); 84 | t.equal(typeof res.tests[0].assertions[0].end, "number"); 85 | t.equal(typeof res.tests[0].assertions[0].start, "number"); 86 | 87 | t.equal(res.tests[1].type, "test"); 88 | t.equal(res.tests[1].name, "# ble"); 89 | t.equal(typeof res.tests[1].start, "number"); 90 | t.equal(typeof res.tests[1].end, "number"); 91 | t.equal(res.tests[1].assertions.length, 5); 92 | 93 | t.equal(res.tests[1].assertions[0].type, "assert"); 94 | t.equal(res.tests[1].assertions[0].number, 2); 95 | t.equal(res.tests[1].assertions[0].diag, null); 96 | t.equal(res.tests[1].assertions[0].name, "got device discover"); 97 | t.equal(typeof res.tests[1].assertions[0].end, "number"); 98 | t.equal(typeof res.tests[1].assertions[0].start, "number"); 99 | 100 | t.equal(res.tests[1].assertions[1].type, "assert"); 101 | t.equal(res.tests[1].assertions[1].number, 3); 102 | t.equal(res.tests[1].assertions[1].diag, null); 103 | t.equal(res.tests[1].assertions[1].name, "device connected"); 104 | t.equal(typeof res.tests[1].assertions[1].end, "number"); 105 | t.equal(typeof res.tests[1].assertions[1].start, "number"); 106 | 107 | t.equal(res.tests[1].assertions[2].type, "assert"); 108 | t.equal(res.tests[1].assertions[2].number, 4); 109 | t.equal(res.tests[1].assertions[2].diag, null); 110 | t.equal(res.tests[1].assertions[2].name, "got characteristic"); 111 | t.equal(typeof res.tests[1].assertions[2].end, "number"); 112 | t.equal(typeof res.tests[1].assertions[2].start, "number"); 113 | 114 | t.equal(res.tests[1].assertions[3].type, "assert"); 115 | t.equal(res.tests[1].assertions[3].number, 5); 116 | t.equal(res.tests[1].assertions[3].diag, null); 117 | t.equal(res.tests[1].assertions[3].name, "subcribed"); 118 | t.equal(typeof res.tests[1].assertions[3].end, "number"); 119 | t.equal(typeof res.tests[1].assertions[3].start, "number"); 120 | 121 | t.equal(res.tests[1].assertions[4].type, "assert"); 122 | t.equal(res.tests[1].assertions[4].number, 6); 123 | t.equal(res.tests[1].assertions[4].diag, null); 124 | t.equal(res.tests[1].assertions[4].name, "got round-trip message"); 125 | t.equal(typeof res.tests[1].assertions[4].end, "number"); 126 | t.equal(typeof res.tests[1].assertions[4].start, "number"); 127 | t.end(); 128 | }) 129 | ); 130 | }); 131 | 132 | t.test("should be able to parse nested test", (t) => { 133 | const flat = fs.createReadStream( 134 | path.resolve(__dirname, "./fixtures/nested.txt") 135 | ); 136 | flat.pipe( 137 | parser((res) => { 138 | t.equal(res.ok, true); 139 | t.equal(res.count, 13); 140 | t.equal(res.pass, 13); 141 | t.equal(res.fail, 0); 142 | t.equal(res.bailout, false); 143 | t.equal(res.todo, 0); 144 | t.equal(res.skip, 0); 145 | t.ok(Array.isArray(res.failures)); 146 | t.equal(res.failures.length, 0); 147 | t.equal(typeof res.time, "number"); 148 | 149 | t.equal(res.tests[0].type, "test"); 150 | t.equal(res.tests[0].name, "# json-ex"); 151 | t.equal(typeof res.tests[0].start, "number"); 152 | t.equal(typeof res.tests[0].end, "number"); 153 | 154 | t.equal(res.tests[0].tests[0].type, "test"); 155 | t.equal( 156 | res.tests[0].tests[0].name, 157 | "# should be able to stringify a basic javascript object" 158 | ); 159 | t.equal(typeof res.tests[0].tests[0].start, "number"); 160 | t.equal(typeof res.tests[0].tests[0].end, "number"); 161 | t.equal(res.tests[0].tests[0].assertions.length, 1); 162 | 163 | t.equal(res.tests[0].tests[1].type, "test"); 164 | t.equal( 165 | res.tests[0].tests[1].name, 166 | "# should be able to stringify a complex javascript object" 167 | ); 168 | t.equal(typeof res.tests[0].tests[1].start, "number"); 169 | t.equal(typeof res.tests[0].tests[1].end, "number"); 170 | t.equal(res.tests[0].tests[1].assertions.length, 1); 171 | 172 | t.equal(res.tests[0].tests[2].type, "test"); 173 | t.equal( 174 | res.tests[0].tests[2].name, 175 | "# should be able to parse a complex object" 176 | ); 177 | t.equal(typeof res.tests[0].tests[2].start, "number"); 178 | t.equal(typeof res.tests[0].tests[2].end, "number"); 179 | t.equal(res.tests[0].tests[2].assertions.length, 4); 180 | 181 | t.equal(res.tests[0].tests[3].type, "test"); 182 | t.equal( 183 | res.tests[0].tests[3].name, 184 | "# should be able to stringify a complex javascript object and parse it back" 185 | ); 186 | t.equal(typeof res.tests[0].tests[3].start, "number"); 187 | t.equal(typeof res.tests[0].tests[3].end, "number"); 188 | t.equal(res.tests[0].tests[3].assertions.length, 7); 189 | t.end(); 190 | }) 191 | ); 192 | }); 193 | 194 | t.test("should be able to parse empty test", (t) => { 195 | const flat = fs.createReadStream( 196 | path.resolve(__dirname, "./fixtures/empty.txt") 197 | ); 198 | flat.pipe( 199 | parser((res) => { 200 | t.equal(res.ok, true); 201 | t.equal(res.count, 0); 202 | t.equal(res.pass, 0); 203 | t.equal(res.fail, 0); 204 | t.equal(res.bailout, false); 205 | t.equal(res.todo, 0); 206 | t.equal(res.skip, 0); 207 | t.ok(Array.isArray(res.failures)); 208 | t.equal(res.failures.length, 0); 209 | t.equal(typeof res.time, "number"); 210 | 211 | t.equal(res.tests[0].type, "test"); 212 | t.equal(res.tests[0].name, "# test this"); 213 | t.equal(typeof res.tests[0].start, "number"); 214 | t.equal(typeof res.tests[0].end, "number"); 215 | t.equal(res.tests[0].tests.length, 0); 216 | 217 | t.end(); 218 | }) 219 | ); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /lib/generate.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const iconClock = (style) => ``; 5 | 6 | const iconPause = ``; 7 | const iconStepForward = ``; 8 | const iconTimes = ``; 9 | const iconCheck = ``; 10 | const iconTasks = ``; 11 | 12 | function parseName (name) { 13 | return name.indexOf('#') === 0 ? name.substring(2, name.length) : name; 14 | } 15 | 16 | function generateStyleString (obj) { 17 | return Object.keys(obj).map((k) => `${k}:${obj[k]}`).join(';'); 18 | } 19 | 20 | function getTotalTests (tests) { 21 | return tests.map((test) => test['tests'] ? getTotalTests(test) : 1).reduce((a, b) => a + b, 0); 22 | } 23 | 24 | function getTotalAssertions (tests) { 25 | return tests.map((test) => test['tests'] ? getTotalAssertions(test) : test['assertions'].length).reduce((a, b) => a + b, 0); 26 | } 27 | 28 | function generateAssertion (assertion) { 29 | const { ok, name, end, start } = assertion; 30 | 31 | return ` 32 |
    33 | ${ok === true 34 | ? `
    35 | ${iconCheck} ${parseName(name)} 36 |
    ` 37 | : `
    38 | ${iconTimes} ${parseName(name)} 39 |
    ` 40 | } 41 |
    ${end - start}ms ${iconClock({ height: '8px', color: 'black' })}
    42 |
43 | `; 44 | } 45 | 46 | function generatePlan (test) { 47 | const { start, end, tests, name, type, assertions } = test; 48 | 49 | const style = {}; 50 | 51 | if (!assertions) { 52 | style['margin-top'] = '50px'; 53 | style['padding-top'] = '50px'; 54 | style['padding-right'] = '15px'; 55 | style['padding-left'] = '15px'; 56 | style['padding-bottom'] = '25px'; 57 | } else { 58 | style['margin-top'] = '60px'; 59 | style['padding-bottom'] = '10px'; 60 | } 61 | 62 | return `
63 |
64 | ${!assertions 65 | ? `

[${type}] ${parseName(name)}

` 66 | : `

[${type}] ${parseName(name)}

` 67 | } 68 |
69 | 70 | ${iconClock({ height: '8px', color: 'black' })} ${end - start}ms   71 | 72 | ${!assertions 73 | ? ` 74 | ${getTotalTests(tests)} tests   75 | ${getTotalAssertions(tests)} assertions 76 |
77 |
` 78 | : ''} 79 |
80 | ${assertions 81 | ? `
    82 | ${assertions.map((assertion) => { 83 | return generateAssertion(assertion); 84 | }).join('')} 85 |
` 86 | : ''} 87 | ${tests 88 | ? tests.map((test) => { 89 | return generatePlan(test); 90 | }).join('') 91 | : '' 92 | } 93 |
`; 94 | } 95 | 96 | function generate (report) { 97 | const { count, pass, fail, skip, time, tests, todo } = report; 98 | 99 | const styles = [ 100 | fs.readFileSync(require.resolve('psychic-ui/dist/psychic-min.css')), 101 | fs.readFileSync(path.resolve(__dirname, 'style.css')) 102 | ]; 103 | 104 | return ` 105 | 106 | 107 | 108 | 109 | tap-html 110 | 111 | 112 | ${ 113 | styles.map((style) => { 114 | return ``; 119 | }).join('') 120 | } 121 | 122 | 123 | 124 |
125 |
126 | 153 |
154 |
155 | ${tests.map((test) => { 156 | return generatePlan(test); 157 | }).join('')} 158 |
159 |
160 |
161 |
162 | 163 | `; 164 | } 165 | 166 | module.exports = { 167 | generate 168 | }; 169 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |
--------------------------------------------------------------------------------