@tapjs/reporter
Version:
Pretty test output reporters for tap
227 lines • 7.31 kB
JavaScript
// junit representation of the test run
// https://github.com/testmoapp/junitxml
import { Minipass } from 'minipass';
import { Parser } from 'tap-parser';
import { stringify } from 'tap-yaml';
const xmlEscape = (s) => s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
const cdata = (s) => s
.split(']]>')
.map(s => `<![CDATA[\n${s.trimEnd()}\n]]>`)
.join(xmlEscape(']]>'));
class Properties {
data;
constructor(data) {
this.data = data;
}
toString() {
/* c8 ignore start */
if (!this.data)
return '';
/* c8 ignore stop */
const body = Object.entries(this.data)
.map(([k, v]) => {
if (v === undefined ||
v === null ||
v === '' ||
v === false) {
return '';
}
else if (typeof v === 'string') {
return v.includes('\n') ?
`<property name="${xmlEscape(k)}">${cdata(v)}</property>`
: `<property name="${xmlEscape(k)}" value="${xmlEscape(v)}" />`;
}
else if (typeof v === 'number' || typeof v === 'boolean') {
return `<property name="${xmlEscape(k)}" value="${xmlEscape(String(v))}" />`;
}
else {
return `<property name="${xmlEscape(k)}">${cdata(stringify(v))}</property>`;
}
})
.filter(v => !!v);
if (!body.length)
return '';
return `<properties>
${body.join('\n')}
</properties>`;
}
}
class Failure {
result;
constructor(result) {
this.result = result;
}
toString() {
const msg = stringify({
...(this.result.diag || { name: this.result.name }),
});
return `<failure>${cdata(msg).trimEnd()}</failure>`;
}
}
class Case {
result;
classname;
constructor(result) {
this.result = result;
const fn = result.fullname;
/* c8 ignore start */
this.classname =
fn.endsWith(result.name) ?
fn
.substring(0, fn.length - result.name.length)
.replace(/>? $/, '')
.trimEnd()
: fn;
/* c8 ignore stop */
}
get failures() {
return this.result.ok ? 0 : 1;
}
get skipped() {
return this.result.skip || this.result.todo ? 1 : 0;
}
get tests() {
return 1;
}
toString() {
/* c8 ignore start */
const file = this.result.diag?.at?.fileName ||
this.result.diag?.at?.file ||
this.result.diag?.file;
const line = this.result.diag?.at?.lineNumber ||
this.result.diag?.at?.line ||
this.result.diag?.line;
const column = this.result.diag?.at?.columnNumber ||
this.result.diag?.at?.column ||
this.result.diag?.column;
/* c8 ignore stop */
const { ok, name, skip, todo, tapError, time, diag } = this.result;
const props = String(new Properties({
...(skip ? { skip } : {}),
...(todo ? { todo } : {}),
/* c8 ignore start */
...(tapError ? { tapError } : {}),
/* c8 ignore stop */
...(diag || {}),
})).trimEnd();
return `<testcase id="${this.result.id}" name="${xmlEscape(name)}" classname="${xmlEscape(this.classname)}"${time ? ` time="${seconds(time)}"` : ''}${file ? ` file="${xmlEscape(String(file))}"` : ''}${file && line ? ` line="${xmlEscape(String(line))}"` : ''}${file && line && column ?
` column="${xmlEscape(String(column))}"`
: ''}${ok && !props ? ' />' : (`>
${!ok ? new Failure(this.result) : props}
</testcase>
`)}`;
}
}
class Suite {
parser;
suites = [];
cases = [];
name;
results;
constructor(parser) {
this.parser = parser;
this.name = parser.name;
this.parser.on('complete', res => (this.results = res));
this.parser.on('child', p => this.suites.push(new Suite(p)));
this.parser.on('assert', a => this.onAssert(a));
}
onAssert(a) {
this.cases.push(new Case(a));
}
get tests() {
return ([...this.suites, ...this.cases]
.map(s => s.tests)
/* c8 ignore start */
.reduce((a, b) => a + b, this.results ? 1 : 0));
/* c8 ignore stop */
}
get failures() {
return [...this.suites, ...this.cases]
.map(s => s.failures)
.reduce((a, b) => a + b, this.results?.ok === false ? 1 : 0);
}
get assertions() {
return this.suites
.map(s => s.assertions)
.reduce((a, b) => a + b, this.cases.length);
}
get skipped() {
return [...this.suites, ...this.cases]
.map(s => s.skipped)
.reduce((a, b) => a + b, (this.results?.plan.skipAll ||
this.results?.skip ||
this.results?.todo) ?
1
: 0);
}
toString() {
const props = new Properties({
ok: this.results?.ok,
plan: this.results?.plan,
bailout: this.results?.bailout,
});
return `<testsuite name="${xmlEscape(this.name)}" tests="${this.tests}" failures="${this.failures}" assertions="${this.assertions}" skipped="${this.skipped}"${this.results?.time ?
` time="${seconds(this.results.time)}"`
: ''}>
${props}
${this.suites
.map(s => String(s))
.join('\n')
.trimEnd()}
${this.cases
.map(c => String(c))
.join('\n')
.trimEnd()}
</testsuite>
`;
}
}
class Suites extends Suite {
onAssert() { }
toString() {
const props = String(new Properties({
ok: this.results?.ok,
plan: this.results?.plan,
bailout: this.results?.bailout,
}));
const id = new Date()
.toISOString()
.replace(/[^0-9T]/g, '')
.replace('T', '_');
return `<testsuites id="${xmlEscape(id)}" name="${xmlEscape(this.name) || 'TAP Tests'}" tests="${this.tests}" failures="${this.failures}" assertions="${this.assertions}" skipped="${this.skipped}" ${
/* c8 ignore start */
this.results?.time ? `time="${seconds(this.results.time)}"` : ''}>${props ? '\n' + props : ''}
${this.suites
/* c8 ignore stop */
.map(s => String(s))
.join('\n')
.trimEnd()}
</testsuites>
`;
}
}
const seconds = (m) => Math.floor(m * 1000) / 1000000;
export class JUnit extends Minipass {
parser = new Parser();
constructor() {
super({ encoding: 'utf8' });
super.write('<?xml version="1.0" encoding="UTF-8" ?>\n');
const suites = new Suites(this.parser);
this.parser.on('complete', () => {
super.write(String(suites));
super.end();
});
}
write(chunk, encoding, cb) {
return this.parser.write(chunk, encoding, cb);
}
end(chunk, encoding, cb) {
this.parser.end(chunk, encoding, cb);
return this;
}
}
//# sourceMappingURL=junit.js.map