test(cli): cover formatLintFindings and normalizeErrorMessage (#1208)

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
This commit is contained in:
Carlos Alcaraz Gregor
2026-06-16 23:56:56 -07:00
committed by GitHub
co-authored by Carlos Alcaraz
parent 513819ee84
commit 8306b2c0b4
2 changed files with 264 additions and 0 deletions
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { normalizeErrorMessage } from "./errorMessage.js";
describe("normalizeErrorMessage", () => {
it("returns the message of an Error instance", () => {
expect(normalizeErrorMessage(new Error("render failed"))).toBe("render failed");
});
it("returns the message of an Error subclass", () => {
expect(normalizeErrorMessage(new TypeError("bad input"))).toBe("bad input");
});
it("passes a plain string through", () => {
expect(normalizeErrorMessage("already a message")).toBe("already a message");
});
it("prefers a string message property on plain objects", () => {
expect(normalizeErrorMessage({ message: "from object", code: 500 })).toBe("from object");
});
it("stringifies plain objects without a message property", () => {
expect(normalizeErrorMessage({ status: 503, retryable: true })).toBe(
'{"status":503,"retryable":true}',
);
});
it("stringifies objects whose message property is not a string", () => {
expect(normalizeErrorMessage({ message: 42 })).toBe('{"message":42}');
});
it("falls back to a key list when JSON.stringify throws on a cycle", () => {
const cyclic: Record<string, unknown> = { code: "E_LOOP" };
cyclic["self"] = cyclic;
expect(normalizeErrorMessage(cyclic)).toBe("{code, self}");
});
it("falls back to String() when an object resists both JSON and key listing", () => {
const hostile = new Proxy(
{},
{
ownKeys() {
throw new Error("no keys for you");
},
},
);
expect(normalizeErrorMessage(hostile)).toBe("[object Object]");
});
it("returns 'unknown error' for null and undefined", () => {
expect(normalizeErrorMessage(null)).toBe("unknown error");
expect(normalizeErrorMessage(undefined)).toBe("unknown error");
});
it("stringifies non-object primitives", () => {
expect(normalizeErrorMessage(42)).toBe("42");
expect(normalizeErrorMessage(false)).toBe("false");
});
});
+206
View File
@@ -0,0 +1,206 @@
import { describe, expect, it } from "vitest";
import type { HyperframeLintFinding } from "@hyperframes/core/lint";
import { formatLintFindings } from "./lintFormat.js";
import type { ProjectLintResult } from "./lintProject.js";
function finding(
severity: HyperframeLintFinding["severity"],
overrides: Partial<HyperframeLintFinding> = {},
): HyperframeLintFinding {
return { code: `${severity}-code`, severity, message: `${severity} message`, ...overrides };
}
function project(
files: Array<{ file: string; findings: HyperframeLintFinding[] }>,
): ProjectLintResult {
const all = files.flatMap((f) => f.findings);
const count = (severity: HyperframeLintFinding["severity"]) =>
all.filter((f) => f.severity === severity).length;
return {
results: files.map(({ file, findings }) => ({
file,
result: {
ok: findings.every((f) => f.severity !== "error"),
errorCount: findings.filter((f) => f.severity === "error").length,
warningCount: findings.filter((f) => f.severity === "warning").length,
infoCount: findings.filter((f) => f.severity === "info").length,
findings,
},
})),
totalErrors: count("error"),
totalWarnings: count("warning"),
totalInfos: count("info"),
};
}
describe("formatLintFindings", () => {
it("returns no lines when no file has findings", () => {
expect(formatLintFindings(project([{ file: "index.html", findings: [] }]))).toEqual([]);
});
it("renders code, message, and elementId without a file label for a single file", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [
finding("error", {
code: "missing-timeline",
message: "no timeline registered",
elementId: "main",
}),
],
},
]),
);
expect(lines).toHaveLength(1);
expect(lines[0]).toContain("✗");
expect(lines[0]).toContain("missing-timeline");
expect(lines[0]).toContain("[main]");
expect(lines[0]).toContain("no timeline registered");
expect(lines[0]).not.toContain("index.html");
});
it("labels each finding with its file when more than one file has results", () => {
const lines = formatLintFindings(
project([
{ file: "index.html", findings: [finding("error")] },
{ file: "compositions/scene.html", findings: [finding("warning")] },
]),
);
expect(lines).toHaveLength(2);
expect(lines[0]).toContain("[index.html]");
expect(lines[1]).toContain("[compositions/scene.html]");
});
it("hides the elementId when showElementId is false", () => {
const result = project([
{ file: "index.html", findings: [finding("error", { elementId: "main" })] },
]);
const lines = formatLintFindings(result, { showElementId: false });
expect(lines[0]).not.toContain("[main]");
});
it("marks warnings with their own prefix and skips info findings by default", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("warning"), finding("info")],
},
]),
);
expect(lines).toHaveLength(1);
expect(lines[0]).toContain("⚠");
expect(lines[0]).toContain("warning message");
});
it("includes info findings when verbose", () => {
const lines = formatLintFindings(
project([{ file: "index.html", findings: [finding("info")] }]),
{ verbose: true },
);
expect(lines).toHaveLength(1);
expect(lines[0]).toContain("");
expect(lines[0]).toContain("info message");
});
it("keeps the input order by default", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("warning"), finding("error")],
},
]),
);
expect(lines).toHaveLength(2);
expect(lines[0]).toContain("warning message");
expect(lines[1]).toContain("error message");
});
it("groups errors before warnings per file when errorsFirst is set", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("warning"), finding("error")],
},
]),
{ errorsFirst: true },
);
expect(lines).toHaveLength(2);
expect(lines[0]).toContain("error message");
expect(lines[1]).toContain("warning message");
});
it("places info findings last under errorsFirst with verbose", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("info"), finding("warning"), finding("error")],
},
]),
{ errorsFirst: true, verbose: true },
);
expect(lines).toHaveLength(3);
expect(lines[0]).toContain("error message");
expect(lines[1]).toContain("warning message");
expect(lines[2]).toContain("info message");
});
it("appends an indented fix hint line directly after its finding", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("error", { fixHint: "add data-composition-id" }), finding("warning")],
},
]),
);
expect(lines).toHaveLength(3);
expect(lines[0]).toContain("error message");
expect(lines[1]).toContain("Fix: add data-composition-id");
expect(lines[2]).toContain("warning message");
});
it("appends a summary line with totals when showSummary is set", () => {
const lines = formatLintFindings(
project([
{
file: "index.html",
findings: [finding("error"), finding("error"), finding("warning")],
},
]),
{ showSummary: true },
);
expect(lines.at(-2)).toBe("");
expect(lines.at(-1)).toContain("2 error(s), 1 warning(s)");
});
it("includes the info count in the summary only when verbose", () => {
const result = project([
{
file: "index.html",
findings: [finding("error"), finding("info")],
},
]);
const quiet = formatLintFindings(result, { showSummary: true });
expect(quiet.at(-1)).not.toContain("info(s)");
const verbose = formatLintFindings(result, { showSummary: true, verbose: true });
expect(verbose.at(-1)).toContain("1 error(s), 0 warning(s), 1 info(s)");
});
});