423 lines
12 KiB
TypeScript
423 lines
12 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
isReportFile,
|
|
isCodeFile,
|
|
classifyFileType,
|
|
reportTitleToSlug,
|
|
panelColSpan,
|
|
formatChartValue,
|
|
formatChartLabel,
|
|
validateReportConfig,
|
|
} from "./report-utils";
|
|
|
|
// ─── isReportFile ───
|
|
|
|
describe("isReportFile", () => {
|
|
it("returns true for .report.json files", () => {
|
|
expect(isReportFile("deals-pipeline.report.json")).toBe(true);
|
|
});
|
|
|
|
it("returns true for deeply nested report files", () => {
|
|
expect(isReportFile("analytics.report.json")).toBe(true);
|
|
});
|
|
|
|
it("returns false for regular JSON", () => {
|
|
expect(isReportFile("config.json")).toBe(false);
|
|
});
|
|
|
|
it("returns false for similarly named non-report files", () => {
|
|
expect(isReportFile("report.json")).toBe(false);
|
|
});
|
|
|
|
it("returns false for markdown", () => {
|
|
expect(isReportFile("report.md")).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty string", () => {
|
|
expect(isReportFile("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── isCodeFile ───
|
|
|
|
describe("isCodeFile", () => {
|
|
it("returns true for .ts files", () => {
|
|
expect(isCodeFile("index.ts")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .tsx files", () => {
|
|
expect(isCodeFile("component.tsx")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .py files", () => {
|
|
expect(isCodeFile("script.py")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .go files", () => {
|
|
expect(isCodeFile("main.go")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .rs files", () => {
|
|
expect(isCodeFile("lib.rs")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .sql files", () => {
|
|
expect(isCodeFile("query.sql")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .yaml files", () => {
|
|
expect(isCodeFile("config.yaml")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .json files", () => {
|
|
expect(isCodeFile("data.json")).toBe(true);
|
|
});
|
|
|
|
it("returns true for .sh files", () => {
|
|
expect(isCodeFile("deploy.sh")).toBe(true);
|
|
});
|
|
|
|
it("returns false for .md files", () => {
|
|
expect(isCodeFile("readme.md")).toBe(false);
|
|
});
|
|
|
|
it("returns false for .txt files", () => {
|
|
expect(isCodeFile("notes.txt")).toBe(false);
|
|
});
|
|
|
|
it("returns false for .png files", () => {
|
|
expect(isCodeFile("image.png")).toBe(false);
|
|
});
|
|
|
|
it("returns true for Makefile (makefile is a code extension)", () => {
|
|
expect(isCodeFile("Makefile")).toBe(true);
|
|
});
|
|
|
|
it("returns false for files with non-code extension", () => {
|
|
expect(isCodeFile("archive.zip")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── classifyFileType ───
|
|
|
|
describe("classifyFileType", () => {
|
|
const mockIsDb = (n: string) => /\.(duckdb|sqlite|sqlite3|db)$/.test(n);
|
|
|
|
it("classifies .report.json as report", () => {
|
|
expect(classifyFileType("test.report.json", mockIsDb)).toBe("report");
|
|
});
|
|
|
|
it("classifies .duckdb as database", () => {
|
|
expect(classifyFileType("workspace.duckdb", mockIsDb)).toBe("database");
|
|
});
|
|
|
|
it("classifies .sqlite as database", () => {
|
|
expect(classifyFileType("data.sqlite", mockIsDb)).toBe("database");
|
|
});
|
|
|
|
it("classifies .md as document", () => {
|
|
expect(classifyFileType("readme.md", mockIsDb)).toBe("document");
|
|
});
|
|
|
|
it("classifies .mdx as document", () => {
|
|
expect(classifyFileType("page.mdx", mockIsDb)).toBe("document");
|
|
});
|
|
|
|
it("classifies .yaml as code", () => {
|
|
expect(classifyFileType("config.yaml", mockIsDb)).toBe("code");
|
|
});
|
|
|
|
it("classifies .ts as code", () => {
|
|
expect(classifyFileType("index.ts", mockIsDb)).toBe("code");
|
|
});
|
|
|
|
it("classifies .txt as file", () => {
|
|
expect(classifyFileType("notes.txt", mockIsDb)).toBe("file");
|
|
});
|
|
|
|
it("report takes priority over other extensions", () => {
|
|
// .report.json should be "report", not "file"
|
|
expect(classifyFileType("x.report.json", mockIsDb)).toBe("report");
|
|
});
|
|
});
|
|
|
|
// ─── reportTitleToSlug ───
|
|
|
|
describe("reportTitleToSlug", () => {
|
|
it("converts simple title to slug", () => {
|
|
expect(reportTitleToSlug("Deals Pipeline")).toBe("deals-pipeline");
|
|
});
|
|
|
|
it("removes special characters", () => {
|
|
expect(reportTitleToSlug("Q1 2025 Revenue (Draft)")).toBe("q1-2025-revenue-draft");
|
|
});
|
|
|
|
it("trims leading/trailing hyphens", () => {
|
|
expect(reportTitleToSlug(" Hello World! ")).toBe("hello-world");
|
|
});
|
|
|
|
it("truncates to 40 characters", () => {
|
|
const long = "A".repeat(100);
|
|
expect(reportTitleToSlug(long).length).toBeLessThanOrEqual(40);
|
|
});
|
|
|
|
it("handles empty string", () => {
|
|
expect(reportTitleToSlug("")).toBe("");
|
|
});
|
|
|
|
it("handles unicode/emoji gracefully", () => {
|
|
const result = reportTitleToSlug("Sales Overview 📊");
|
|
expect(result).toBe("sales-overview");
|
|
expect(result).not.toContain("📊");
|
|
});
|
|
|
|
it("collapses multiple dashes", () => {
|
|
expect(reportTitleToSlug("a --- b")).toBe("a-b");
|
|
});
|
|
});
|
|
|
|
// ─── panelColSpan ───
|
|
|
|
describe("panelColSpan", () => {
|
|
it("returns col-span-6 for full", () => {
|
|
expect(panelColSpan("full")).toBe("col-span-6");
|
|
});
|
|
|
|
it("returns col-span-3 for half", () => {
|
|
expect(panelColSpan("half")).toBe("col-span-3");
|
|
});
|
|
|
|
it("returns col-span-2 for third", () => {
|
|
expect(panelColSpan("third")).toBe("col-span-2");
|
|
});
|
|
|
|
it("returns col-span-3 for undefined (default)", () => {
|
|
expect(panelColSpan(undefined)).toBe("col-span-3");
|
|
});
|
|
|
|
it("returns col-span-3 for unknown size", () => {
|
|
expect(panelColSpan("quarter")).toBe("col-span-3");
|
|
});
|
|
});
|
|
|
|
// ─── formatChartValue ───
|
|
|
|
describe("formatChartValue", () => {
|
|
it("returns empty string for null", () => {
|
|
expect(formatChartValue(null)).toBe("");
|
|
});
|
|
|
|
it("returns empty string for undefined", () => {
|
|
expect(formatChartValue(undefined)).toBe("");
|
|
});
|
|
|
|
it("formats millions", () => {
|
|
expect(formatChartValue(1_500_000)).toBe("1.5M");
|
|
});
|
|
|
|
it("formats thousands", () => {
|
|
expect(formatChartValue(1_500)).toBe("1.5K");
|
|
});
|
|
|
|
it("formats negative millions", () => {
|
|
expect(formatChartValue(-2_500_000)).toBe("-2.5M");
|
|
});
|
|
|
|
it("formats negative thousands", () => {
|
|
expect(formatChartValue(-2_500)).toBe("-2.5K");
|
|
});
|
|
|
|
it("formats integers below 1000 as-is", () => {
|
|
expect(formatChartValue(42)).toBe("42");
|
|
});
|
|
|
|
it("formats floats to 2 decimal places", () => {
|
|
expect(formatChartValue(Math.PI)).toBe("3.14");
|
|
});
|
|
|
|
it("formats zero as integer", () => {
|
|
expect(formatChartValue(0)).toBe("0");
|
|
});
|
|
|
|
it("formats strings as-is", () => {
|
|
expect(formatChartValue("hello")).toBe("hello");
|
|
});
|
|
|
|
it("formats boolean as string", () => {
|
|
expect(formatChartValue(true)).toBe("true");
|
|
});
|
|
|
|
it("formats exactly 1000", () => {
|
|
expect(formatChartValue(1000)).toBe("1.0K");
|
|
});
|
|
|
|
it("formats exactly 1000000", () => {
|
|
expect(formatChartValue(1000000)).toBe("1.0M");
|
|
});
|
|
|
|
it("formats 999 as integer", () => {
|
|
expect(formatChartValue(999)).toBe("999");
|
|
});
|
|
|
|
it("formats object as JSON string", () => {
|
|
expect(formatChartValue({ key: "val" })).toBe('{"key":"val"}');
|
|
});
|
|
|
|
it("formats array as JSON string", () => {
|
|
expect(formatChartValue([1, 2, 3])).toBe("[1,2,3]");
|
|
});
|
|
|
|
it("formats negative float", () => {
|
|
expect(formatChartValue(-3.14)).toBe("-3.14");
|
|
});
|
|
|
|
it("formats very large number", () => {
|
|
expect(formatChartValue(999_999_999)).toBe("1000.0M");
|
|
});
|
|
});
|
|
|
|
// ─── formatChartLabel ───
|
|
|
|
describe("formatChartLabel", () => {
|
|
it("returns empty string for null", () => {
|
|
expect(formatChartLabel(null)).toBe("");
|
|
});
|
|
|
|
it("returns empty string for undefined", () => {
|
|
expect(formatChartLabel(undefined)).toBe("");
|
|
});
|
|
|
|
it("returns short strings unchanged", () => {
|
|
expect(formatChartLabel("Active")).toBe("Active");
|
|
});
|
|
|
|
it("truncates long strings", () => {
|
|
const long = "A".repeat(25);
|
|
expect(formatChartLabel(long)).toBe("A".repeat(18) + "...");
|
|
});
|
|
|
|
it("shortens ISO date strings", () => {
|
|
expect(formatChartLabel("2025-06-15T10:30:00Z")).toBe("2025-06-15");
|
|
});
|
|
|
|
it("shortens full datetime strings", () => {
|
|
expect(formatChartLabel("2025-06-15 10:30:00.000")).toBe("2025-06-15");
|
|
});
|
|
|
|
it("does not shorten non-date long strings", () => {
|
|
const notDate = "This is definitely not a date string at all";
|
|
expect(formatChartLabel(notDate)).toBe("This is definitely..." );
|
|
});
|
|
|
|
it("handles numbers by converting to string", () => {
|
|
expect(formatChartLabel(42)).toBe("42");
|
|
});
|
|
|
|
it("handles exactly 20-char string (no truncation)", () => {
|
|
expect(formatChartLabel("12345678901234567890")).toBe("12345678901234567890");
|
|
});
|
|
|
|
it("truncates 21-char string", () => {
|
|
expect(formatChartLabel("123456789012345678901")).toBe("123456789012345678...");
|
|
});
|
|
});
|
|
|
|
// ─── validateReportConfig ───
|
|
|
|
describe("validateReportConfig", () => {
|
|
const validConfig = {
|
|
version: 1,
|
|
title: "Test",
|
|
panels: [
|
|
{ id: "p1", title: "P1", type: "bar", sql: "SELECT 1", mapping: { xAxis: "x" } },
|
|
],
|
|
};
|
|
|
|
it("returns null for valid config", () => {
|
|
expect(validateReportConfig(validConfig)).toBeNull();
|
|
});
|
|
|
|
it("returns null for valid config with filters", () => {
|
|
expect(validateReportConfig({
|
|
...validConfig,
|
|
filters: [{ id: "f1", type: "dateRange", label: "Date", column: "created_at" }],
|
|
})).toBeNull();
|
|
});
|
|
|
|
it("rejects null config", () => {
|
|
expect(validateReportConfig(null)).not.toBeNull();
|
|
});
|
|
|
|
it("rejects non-object config", () => {
|
|
expect(validateReportConfig("string")).not.toBeNull();
|
|
});
|
|
|
|
it("rejects missing title", () => {
|
|
expect(validateReportConfig({ panels: [] })).toContain("title");
|
|
});
|
|
|
|
it("rejects empty title", () => {
|
|
expect(validateReportConfig({ title: "", panels: [] })).toContain("title");
|
|
});
|
|
|
|
it("rejects missing panels", () => {
|
|
expect(validateReportConfig({ title: "Test" })).toContain("panels");
|
|
});
|
|
|
|
it("rejects non-array panels", () => {
|
|
expect(validateReportConfig({ title: "Test", panels: "not-array" })).toContain("panels");
|
|
});
|
|
|
|
it("accepts empty panels array", () => {
|
|
expect(validateReportConfig({ title: "Test", panels: [] })).toBeNull();
|
|
});
|
|
|
|
it("rejects panel without id", () => {
|
|
const config = { title: "Test", panels: [{ title: "P", type: "bar", sql: "SELECT 1", mapping: {} }] };
|
|
expect(validateReportConfig(config)).toContain("Panel 0");
|
|
expect(validateReportConfig(config)).toContain("id");
|
|
});
|
|
|
|
it("rejects panel without title", () => {
|
|
const config = { title: "Test", panels: [{ id: "p", type: "bar", sql: "SELECT 1", mapping: {} }] };
|
|
expect(validateReportConfig(config)).toContain("title");
|
|
});
|
|
|
|
it("rejects panel without type", () => {
|
|
const config = { title: "Test", panels: [{ id: "p", title: "P", sql: "SELECT 1", mapping: {} }] };
|
|
expect(validateReportConfig(config)).toContain("type");
|
|
});
|
|
|
|
it("rejects panel without sql", () => {
|
|
const config = { title: "Test", panels: [{ id: "p", title: "P", type: "bar", mapping: {} }] };
|
|
expect(validateReportConfig(config)).toContain("sql");
|
|
});
|
|
|
|
it("rejects panel without mapping", () => {
|
|
const config = { title: "Test", panels: [{ id: "p", title: "P", type: "bar", sql: "SELECT 1" }] };
|
|
expect(validateReportConfig(config)).toContain("mapping");
|
|
});
|
|
|
|
it("validates multiple panels", () => {
|
|
const config = {
|
|
title: "Test",
|
|
panels: [
|
|
{ id: "p1", title: "P1", type: "bar", sql: "SELECT 1", mapping: {} },
|
|
{ id: "p2", title: "P2", type: "pie", sql: "SELECT 2", mapping: {} },
|
|
],
|
|
};
|
|
expect(validateReportConfig(config)).toBeNull();
|
|
});
|
|
|
|
it("reports correct panel index on validation error", () => {
|
|
const config = {
|
|
title: "Test",
|
|
panels: [
|
|
{ id: "p1", title: "P1", type: "bar", sql: "SELECT 1", mapping: {} },
|
|
{ id: "p2", type: "pie", sql: "SELECT 2", mapping: {} }, // missing title
|
|
],
|
|
};
|
|
expect(validateReportConfig(config)).toContain("Panel 1");
|
|
});
|
|
});
|