@japa/runner
Version:
A simple yet powerful testing framework for Node.js
348 lines (341 loc) • 9.82 kB
JavaScript
import {
BaseReporter,
colors,
icons
} from "./chunk-PCBL2VZP.js";
// src/reporters/dot.ts
var DotReporter = class extends BaseReporter {
/**
* When a test ended
*/
onTestEnd(payload) {
let output = "";
if (payload.isTodo) {
output = colors.cyan(icons.info);
} else if (payload.hasError) {
output = colors.red(icons.cross);
} else if (payload.isSkipped) {
output = colors.yellow(icons.bullet);
} else if (payload.isFailing) {
output = colors.magenta(icons.squareSmallFilled);
} else {
output = colors.green(icons.tick);
}
process.stdout.write(`${output}`);
}
/**
* When test runner ended
*/
async end() {
console.log("");
await this.printSummary(this.runner.getSummary());
}
};
// src/reporters/spec.ts
import ms from "ms";
import { relative } from "path";
var SpecReporter = class extends BaseReporter {
/**
* Tracking if the first event we get is for a test without any parent group
* We need this to decide the display style for tests without groups.
*/
#isFirstLoneTest = true;
/**
* Returns the icon for the test
*/
#getTestIcon(payload) {
if (payload.isTodo) {
return colors.cyan(icons.info);
}
if (payload.hasError) {
return colors.red(icons.cross);
}
if (payload.isSkipped) {
return colors.yellow(icons.bullet);
}
if (payload.isFailing) {
return colors.magenta(icons.squareSmallFilled);
}
return colors.green(icons.tick);
}
/**
* Returns the test message
*/
#getTestMessage(payload) {
const message = payload.title.expanded;
if (payload.isTodo) {
return colors.blue(message);
}
if (payload.hasError) {
return colors.red(message);
}
if (payload.isSkipped) {
return colors.yellow(message);
}
if (payload.isFailing) {
return colors.magenta(message);
}
return colors.grey(message);
}
/**
* Returns the subtext message for the test
*/
#getSubText(payload) {
if (payload.isSkipped && payload.skipReason) {
return colors.dim(`${icons.branch} ${colors.italic(payload.skipReason)}`);
}
if (!payload.isFailing) {
return;
}
if (payload.hasError) {
const message = payload.errors.find((error) => error.phase === "test")?.error.message ?? `Test marked with ".fails()" must finish with an error`;
return colors.dim(`${icons.branch} ${colors.italic(message)}`);
}
if (payload.failReason) {
return colors.dim(`${icons.branch} ${colors.italic(payload.failReason)}`);
}
const testErrorMessage = payload.errors.find((error) => error.phase === "test");
if (testErrorMessage && testErrorMessage.error) {
return colors.dim(`${icons.branch} ${colors.italic(testErrorMessage.error.message)}`);
}
}
/**
* Returns the filename relative from the current working dir
*/
#getRelativeFilename(fileName) {
return relative(process.cwd(), fileName);
}
/**
* Prints the test details
*/
#printTest(payload) {
const icon = this.#getTestIcon(payload);
const message = this.#getTestMessage(payload);
const prefix = payload.isPinned ? colors.yellow("[PINNED] ") : "";
const indentation = this.currentFileName || this.currentGroupName ? " " : "";
const duration = colors.dim(`(${ms(Number(payload.duration.toFixed(2)))})`);
const retries = payload.retryAttempt && payload.retryAttempt > 1 ? colors.dim(`(x${payload.retryAttempt}) `) : "";
let subText = this.#getSubText(payload);
subText = subText ? `
${indentation} ${subText}` : "";
console.log(`${indentation}${icon} ${prefix}${retries}${message} ${duration}${subText}`);
}
/**
* Prints the group name
*/
#printGroup(payload) {
const title = this.currentSuiteName !== "default" ? `${this.currentSuiteName} / ${payload.title}` : payload.title;
const suffix = this.currentFileName ? colors.dim(` (${this.#getRelativeFilename(this.currentFileName)})`) : "";
console.log(`
${title}${suffix}`);
}
onTestStart() {
if (this.currentFileName && this.#isFirstLoneTest) {
console.log(`
${colors.dim(this.#getRelativeFilename(this.currentFileName))}`);
}
this.#isFirstLoneTest = false;
}
onTestEnd(payload) {
this.#printTest(payload);
}
onGroupStart(payload) {
this.#isFirstLoneTest = false;
this.#printGroup(payload);
}
onGroupEnd() {
this.#isFirstLoneTest = true;
}
async end() {
const summary = this.runner.getSummary();
await this.printSummary(summary);
}
};
// src/reporters/ndjson.ts
import { relative as relative2 } from "path";
import { serializeError } from "serialize-error";
var NdJSONReporter = class extends BaseReporter {
/**
* Returns the filename relative from the current working dir
*/
#getRelativeFilename(fileName) {
return relative2(process.cwd(), fileName);
}
/**
* Serialize errors to JSON
*/
#serializeErrors(errors) {
return errors.map((error) => ({
phase: error.phase,
error: serializeError(error.error)
}));
}
onTestEnd(payload) {
console.log(
JSON.stringify({
event: "test:end",
filePath: this.currentFileName,
relativePath: this.currentFileName ? this.#getRelativeFilename(this.currentFileName) : void 0,
title: payload.title,
duration: payload.duration,
failReason: payload.failReason,
isFailing: payload.isFailing,
skipReason: payload.skipReason,
isSkipped: payload.isSkipped,
isTodo: payload.isTodo,
isPinned: payload.isPinned,
retryAttempt: payload.retryAttempt,
retries: payload.retries,
errors: this.#serializeErrors(payload.errors)
})
);
}
onGroupStart(payload) {
console.log(
JSON.stringify({
event: "group:start",
title: payload.title
})
);
}
onGroupEnd(payload) {
JSON.stringify({
event: "group:end",
title: payload.title,
errors: this.#serializeErrors(payload.errors)
});
}
onSuiteStart(payload) {
console.log(
JSON.stringify({
event: "suite:start",
...payload
})
);
}
onSuiteEnd(payload) {
console.log(
JSON.stringify({
event: "suite:end",
name: payload.name,
hasError: payload.hasError,
errors: this.#serializeErrors(payload.errors)
})
);
}
async end() {
const summary = this.runner.getSummary();
console.log(
JSON.stringify({
aggregates: summary.aggregates,
duration: summary.duration,
failedTestsTitles: summary.failedTestsTitles,
hasError: summary.hasError
})
);
}
};
// src/reporters/github.ts
import slash from "slash";
import { relative as relative3 } from "path";
import { stripVTControlCharacters } from "util";
import { ErrorsPrinter } from "@japa/errors-printer";
var GithubReporter = class extends BaseReporter {
/**
* Performs string escape on annotation message as per
* https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
*/
escapeMessage(value) {
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
}
/**
* Performs string escape on annotation properties as per
* https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
*/
escapeProperty(value) {
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
}
/**
* Formats the message as per the Github annotations spec.
* https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
*/
formatMessage({
command,
properties,
message
}) {
let result = `::${command}`;
Object.entries(properties).forEach(([k, v], i) => {
result += i === 0 ? " " : ",";
result += `${k}=${this.escapeProperty(v)}`;
});
result += `::${this.escapeMessage(message)}`;
return result;
}
/**
* Prints Github annotation for a given error
*/
async getErrorAnnotation(printer, error) {
const parsedError = await printer.parseError(error.error);
if (!("frames" in parsedError)) {
return;
}
const mainFrame = parsedError.frames.find((frame) => frame.type === "app");
if (!mainFrame) {
return;
}
return this.formatMessage({
command: "error",
properties: {
file: slash(relative3(process.cwd(), mainFrame.fileName)),
title: error.title,
line: String(mainFrame.lineNumber),
column: String(mainFrame.columnNumber)
},
message: stripVTControlCharacters(parsedError.message)
});
}
async end() {
const summary = this.runner.getSummary();
const errorsList = this.aggregateErrors(summary);
const errorPrinter = new ErrorsPrinter(this.options);
for (let error of errorsList) {
const formatted = await this.getErrorAnnotation(errorPrinter, error);
if (formatted) {
console.log(`
${formatted}`);
}
}
}
};
// src/reporters/main.ts
var spec = (options) => {
return {
name: "spec",
handler: (...args) => new SpecReporter(options).boot(...args)
};
};
var dot = (options) => {
return {
name: "dot",
handler: (...args) => new DotReporter(options).boot(...args)
};
};
var ndjson = (options) => {
return {
name: "ndjson",
handler: (...args) => new NdJSONReporter(options).boot(...args)
};
};
var github = (options) => {
return {
name: "github",
handler: (...args) => new GithubReporter(options).boot(...args)
};
};
export {
spec,
dot,
ndjson,
github
};