@prantlf/faucet
Version:
Human-readable TAP reporter/formatter.
290 lines (270 loc) • 7.91 kB
JavaScript
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 ''
}
}