UNPKG

@prantlf/faucet

Version:

Human-readable TAP reporter/formatter.

290 lines (270 loc) 7.91 kB
const through2 = require('through2') const duplexer = require('duplexer3') const Parser = require('tap-parser') const { sprintf } = require('sprintf-js') module.exports = opts => { const { verbose, width } = opts || {} const tap = new Parser() const out = through2() let test, lastComment let id = 0 const epilog = new Set() tap.on('comment', comment => { if (comment === 'fail 0') return // a mocha thing let hide if (test && test.ok) { if (test.assertions.length === 0) { let match if ((match = /^tests\s+(\d+)$/.exec(test.name))) { epilog.add('tests') if (+match[1]) { out.push('\r' + updateName(test.offset + 1, '# ' + test.name, 1)) } else { hide = true } } else if ((match = /^pass\s+\d+$/.exec(test.name))) { epilog.add('pass') if (+match[1]) { out.push('\r' + updateName(test.offset + 1, '✓ ' + test.name, 32)) } else { hide = true } } else if ((match = /^fail\s+\d+$/.exec(test.name))) { epilog.add('fail') if (+match[1]) { out.push('\r' + updateName(test.offset + 1, '⨯ ' + test.name, 31)) } else { hide = true } } else if ((match = /^todo\s+\d+$/.exec(test.name))) { epilog.add('todo') if (+match[1]) { out.push('\r' + updateName(test.offset + 1, '> ' + test.name, 35)) } else { hide = true } } else if ((match = /^skip\s+\d+$/.exec(test.name))) { epilog.add('skip') if (+match[1]) { out.push('\r' + updateName(test.offset + 1, '- ' + test.name, 34)) } else { hide = true } } else if ((match = /^test count\(\d+\) != plan\([^)]+\)$/.exec(test.name))) { out.push('\r' + updateName(test.offset + 1, '? ' + test.name, 33)) } else { hide = true } } else { const skipped = test.assertions.filter(({ skip }) => skip) let s if (skipped.length) { if (skipped.length === test.assertions.length) { s = updateName(test.offset + 1, '- ' + test.name, 34) } else { s = updateName(test.offset + 1, '✓ ' + test.name + ' (some skipped)', 32) } } else { s = updateName(test.offset + 1, '✓ ' + test.name, 32) } out.push('\r' + s) } } test = { name: comment.startsWith('#') ? comment.substring(1).trim() : comment.trim(), assertions: [], offset: 0, ok: true } if (!hide) { out.push('\r\x1b[K\n') } lastComment = true }) tap.on('assert', res => { lastComment = false const ok = res.ok ? 'ok' : 'not ok' const c = res.ok ? 32 : res.todo ? 35 : 31 if (!test) { // mocha produces TAP results this way, whatever const s = res.name ? trim(res.name.trim()) : '' out.push(sprintf( '\x1b[1m\x1b[' + c + 'm%s\x1b[0m\n', trim((res.ok ? '✓' : res.todo ? '>' : '⨯') + ' ' + s, res.ok) )) const diag = printDiag(res.diag) if (diag) { out.push(diag) } return } if (!res.id) { res.id = ++id } const fmt = '\r %s \x1b[1m\x1b[' + c + 'm%d\x1b[0m %s\x1b[K' let str = sprintf(fmt, ok, res.id, res.name ? trim(res.name, res.ok) : '') if (res.ok && verbose) { str += '\n' } if (!res.ok) { const y = (++test.offset) + 1 str += '\n' if (test.ok) { if (verbose) { str += '\n' } str += updateName(y, (res.todo ? '>' : '⨯') + ' ' + test.name, res.todo ? 35 : 31) } str += printDiag(res.diag) test.ok = false } out.push(str) test.assertions.push(res) }) tap.on('extra', extra => { if (!test || test.assertions.length === 0) return const last = test.assertions[test.assertions.length - 1] if (!last.ok) { out.push(extra.split('\n').map(line => ` ${line}`).join('\n') + '\n') } }) tap.on('complete', res => { let line const lineBreak = () => { if (line) { out.push('\n') } else { line = true } } let empty if (!lastComment) { out.push('\r\x1b[K\n') } if (res.bailout) { lineBreak() out.push(updateName(1, '! ' + res.bailout, 31)) } if (res.plan.skipReason) { lineBreak() if (res.plan.skipReason === 'no tests found') { empty = true out.push(updateName(1, '! ' + res.plan.skipReason, 31)) } else { out.push(updateName(1, '- ' + res.plan.skipReason, 34)) } } let pass = res.pass - res.skip let fail = res.fail - res.todo if (fail < 0) { pass -= res.todo fail = 0 } if (!test) { if (res.count) { lineBreak() out.push(updateName(1, '# tests ' + res.count, 1)) } if (pass) { lineBreak() out.push(updateName(1, '✓ pass ' + pass, 32)) } if (fail) { lineBreak() out.push(updateName(1, '⨯ fail ' + fail, 31)) } if (res.todo) { lineBreak() out.push(updateName(1, '> todo ' + res.todo, 35)) } if (res.skip) { lineBreak() out.push(updateName(1, '- skip ' + res.skip, 34)) } } else { if (!epilog.has('tests') && res.count) { lineBreak() out.push(updateName(test.offset + 1, '# tests ' + res.count, 1)) } if (!epilog.has('pass') && pass) { lineBreak() out.push(updateName(test.offset + 1, '✓ pass ' + pass, 32)) } if (!epilog.has('fail') && fail) { lineBreak() out.push(updateName(test.offset + 1, '⨯ fail ' + fail, 31)) } if (!epilog.has('todo') && res.todo) { lineBreak() out.push(updateName(test.offset + 1, '> todo ' + res.todo, 35)) } if (!epilog.has('skip') && res.skip) { lineBreak() out.push(updateName(test.offset + 1, '- skip ' + res.skip, 34)) } } out.push(null) dup.emit('complete', res) if (!res.ok || empty) dup.emit('fail') dup.exitCode = res.ok && !empty ? 0 : 1 }) const dup = duplexer(tap, out) return dup function trim (s, ok) { if (width && s.length > width - 2) { const more = ok ? 0 : 4 s = s.slice(0, width - 5 - more) + '...' } return s } function updateName (y, str, c) { return '\x1b[' + y + 'A' + '\x1b[1G' + '\x1b[1m\x1b[' + c + 'm' + trim(str) + '\x1b[0m' + '\x1b[' + y + 'B\x1b[1G' } function printArray (lines, indent, items) { for (const item of items) { lines.push(indent + item + '\n') } } function printObject (lines, indent, diag) { for (const key in diag) { let val = diag[key] if (key === 'stack') { if (verbose) { val = '\n' + val .split('\n') .map((line, index) => index ? ` ${line}` : ` ${line}`) .join('\n') } else { continue } } if (val) { if (Array.isArray(val)) { lines.push(indent + (key + ':').padEnd(10) + '\n') printArray(lines, indent + ' ', val) continue } if (typeof val === 'object') { lines.push(indent + (key + ':').padEnd(10) + '\n') printObject(lines, indent + ' ', val) continue } } lines.push(indent + (key + ':').padEnd(10) + val + '\n') } } function printDiag (diag) { if (diag) { const lines = [' ---\n'] printObject(lines, ' ', diag) lines.push(' ...\n') return lines.join('') } return '' } }