UNPKG

@japa/runner

Version:

A simple yet powerful testing framework for Node.js

348 lines (341 loc) 9.82 kB
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 };