UNPKG

@japa/runner

Version:

A simple yet powerful testing framework for Node.js

426 lines (425 loc) 13.4 kB
import { f as BaseReporter, r as icons, t as colors } from "./helpers-C1bD1eod.js"; import { ErrorsPrinter } from "@japa/errors-printer"; import { stripVTControlCharacters } from "node:util"; import { relative } from "node:path"; import string from "@poppinss/string"; var DotReporter = class extends BaseReporter { 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}`); } async end() { console.log(""); await this.printSummary(this.runner.getSummary()); } }; var SpecReporter = class extends BaseReporter { #isFirstLoneTest = true; #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); } #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); } #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)}`); } #getRelativeFilename(fileName) { return relative(process.cwd(), fileName); } #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(`(${string.milliseconds.format(Number(payload.duration.toFixed(2)))})`); const retries = payload.retryAttempt && payload.retryAttempt > 1 ? colors.dim(`(x${payload.retryAttempt}) `) : ""; let subText = this.#getSubText(payload); subText = subText ? `\n${indentation} ${subText}` : ""; console.log(`${indentation}${icon} ${prefix}${retries}${message} ${duration}${subText}`); } #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(`\n${title}${suffix}`); } onTestStart() { if (this.currentFileName && this.#isFirstLoneTest) console.log(`\n${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); } }; const isNonErrorSymbol = Symbol("isNonError"); function defineProperty(object, key, value) { Object.defineProperty(object, key, { value, writable: false, enumerable: false, configurable: false }); } function stringify(value) { if (value === void 0) return "undefined"; if (value === null) return "null"; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (typeof value === "bigint") return `${value}n`; if (typeof value === "symbol") return value.toString(); if (typeof value === "function") return `[Function${value.name ? ` ${value.name}` : " (anonymous)"}]`; if (value instanceof Error) try { return String(value); } catch { return "<Unserializable error>"; } try { return JSON.stringify(value); } catch { try { return String(value); } catch { return "<Unserializable value>"; } } } var NonError = class NonError extends Error { constructor(value, { superclass: Superclass = Error } = {}) { if (NonError.isNonError(value)) return value; if (value instanceof Error) throw new TypeError("Do not pass Error instances to NonError. Throw the error directly instead."); super(`Non-error value: ${stringify(value)}`); if (Superclass !== Error) Object.setPrototypeOf(this, Superclass.prototype); defineProperty(this, "name", "NonError"); defineProperty(this, isNonErrorSymbol, true); defineProperty(this, "isNonError", true); defineProperty(this, "value", value); } static isNonError(value) { return value?.[isNonErrorSymbol] === true; } static #handleCallback(callback, arguments_) { try { const result = callback(...arguments_); if (result && typeof result.then === "function") return (async () => { try { return await result; } catch (error) { if (error instanceof Error) throw error; throw new NonError(error); } })(); return result; } catch (error) { if (error instanceof Error) throw error; throw new NonError(error); } } static try(callback) { return NonError.#handleCallback(callback, []); } static wrap(callback) { return (...arguments_) => NonError.#handleCallback(callback, arguments_); } static [Symbol.hasInstance](instance) { return NonError.isNonError(instance); } }; const list = [ Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, AggregateError, globalThis.DOMException, globalThis.AssertionError, globalThis.SystemError ].filter(Boolean).map((constructor) => [constructor.name, constructor]); const errorConstructors = new Map(list); const errorFactories = /* @__PURE__ */ new Map(); const errorProperties = [ { property: "name", enumerable: false }, { property: "message", enumerable: false }, { property: "stack", enumerable: false }, { property: "code", enumerable: true }, { property: "cause", enumerable: false }, { property: "errors", enumerable: false } ]; const toJsonWasCalled = /* @__PURE__ */ new WeakSet(); const toJSON = (from) => { toJsonWasCalled.add(from); const json = from.toJSON(); toJsonWasCalled.delete(from); return json; }; const newError = (name) => { const factory = errorFactories.get(name); if (factory) return factory(); const ErrorConstructor = errorConstructors.get(name) ?? Error; return ErrorConstructor === AggregateError ? new ErrorConstructor([]) : new ErrorConstructor(); }; const destroyCircular = ({ from, seen, to, forceEnumerable, maxDepth, depth, useToJSON, serialize }) => { if (!to) if (Array.isArray(from)) to = []; else if (!serialize && isErrorLike(from)) to = newError(from.name); else to = {}; seen.push(from); if (depth >= maxDepth) return to; if (useToJSON && typeof from.toJSON === "function" && !toJsonWasCalled.has(from)) return toJSON(from); const continueDestroyCircular = (value) => destroyCircular({ from: value, seen: [...seen], forceEnumerable, maxDepth, depth: depth + 1, useToJSON, serialize }); for (const [key, value] of Object.entries(from)) { if (value && value instanceof Uint8Array && value.constructor.name === "Buffer") { to[key] = serialize ? "[object Buffer]" : value; continue; } if (value !== null && typeof value === "object" && typeof value.pipe === "function") { to[key] = serialize ? "[object Stream]" : value; continue; } if (typeof value === "function") { if (!serialize) to[key] = value; continue; } if (serialize && typeof value === "bigint") { to[key] = `${value}n`; continue; } if (!value || typeof value !== "object") { try { to[key] = value; } catch {} continue; } if (!seen.includes(value)) { to[key] = continueDestroyCircular(value); continue; } to[key] = "[Circular]"; } if (serialize || to instanceof Error) { for (const { property, enumerable } of errorProperties) if (from[property] !== void 0 && from[property] !== null) Object.defineProperty(to, property, { value: isErrorLike(from[property]) || Array.isArray(from[property]) ? continueDestroyCircular(from[property]) : from[property], enumerable: forceEnumerable ? true : enumerable, configurable: true, writable: true }); } return to; }; function serializeError(value, options = {}) { const { maxDepth = Number.POSITIVE_INFINITY, useToJSON = true } = options; if (typeof value === "object" && value !== null) return destroyCircular({ from: value, seen: [], forceEnumerable: true, maxDepth, depth: 0, useToJSON, serialize: true }); if (typeof value === "function") value = "<Function>"; return destroyCircular({ from: new NonError(value), seen: [], forceEnumerable: true, maxDepth, depth: 0, useToJSON, serialize: true }); } function isErrorLike(value) { return Boolean(value) && typeof value === "object" && typeof value.name === "string" && typeof value.message === "string" && typeof value.stack === "string"; } var NdJSONReporter = class extends BaseReporter { #getRelativeFilename(fileName) { return relative(process.cwd(), fileName); } #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 })); } }; var GithubReporter = class extends BaseReporter { escapeMessage(value) { return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); } escapeProperty(value) { return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C"); } 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; } 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: string.toUnixSlash(relative(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(`\n${formatted}`); } } }; const spec = (options) => { return { name: "spec", handler: (...args) => new SpecReporter(options).boot(...args) }; }; const dot = (options) => { return { name: "dot", handler: (...args) => new DotReporter(options).boot(...args) }; }; const ndjson = (options) => { return { name: "ndjson", handler: (...args) => new NdJSONReporter(options).boot(...args) }; }; const github = (options) => { return { name: "github", handler: (...args) => new GithubReporter(options).boot(...args) }; }; export { spec as i, github as n, ndjson as r, dot as t };