import { describe, expect, it } from "@suaveplan/testing/runner"; import { PROFILER_CODES, type ProfilerError, } from "../error-codes/error-codes.js"; import type { ProfileEntry, ProfileMark } from "../types/profiler.types.js"; import { ProfileReporter } from "./reporter.js"; function entry( label: string, duration: number, category?: string ): ProfileEntry { return { label, startTime: 0, endTime: duration, duration, category }; } describe("ProfileReporter", () => { it("throws INVALID_OPTIONS for non-empty bad percentiles", { timeout: 5000, }, () => { try { new ProfileReporter({ percentiles: [200] }); throw new Error("expected throw"); } catch (err) { expect((err as ProfilerError).code).toBe( PROFILER_CODES.INVALID_OPTIONS ); } }); it("throws INVALID_OPTIONS for empty percentiles", { timeout: 5000, }, () => { try { new ProfileReporter({ percentiles: [] }); throw new Error("expected throw"); } catch (err) { expect((err as ProfilerError).code).toBe( PROFILER_CODES.INVALID_OPTIONS ); } }); it("computes percentiles", { timeout: 5000 }, () => { const r = new ProfileReporter({ percentiles: [50, 90], clock: () => 0, }); const stats = r.computeStats([10, 20, 30, 40, 50]); expect(stats.count).toBe(5); expect(stats.min).toBe(10); expect(stats.max).toBe(50); expect(stats.mean).toBe(30); expect(stats.median).toBe(30); expect(stats.percentiles[50]).toBe(30); expect(stats.percentiles[90]).toBeGreaterThan(30); }); it("handles empty entries", { timeout: 5000 }, () => { const r = new ProfileReporter(); const stats = r.computeStats([]); expect(stats).toEqual({ count: 0, min: 0, max: 0, mean: 0, median: 0, percentiles: {}, totalDuration: 0, }); }); it("groups by category and uncategorized falls back", { timeout: 5000, }, () => { const r = new ProfileReporter({ clock: () => 0 }); const report = r.generateReport([ entry("a", 10, "render"), entry("b", 20, "render"), entry("c", 30), ]); expect(report.byCategory.render?.count).toBe(2); expect(report.byCategory.uncategorized?.count).toBe(1); }); it("flags alerts above threshold", { timeout: 5000 }, () => { const r = new ProfileReporter({ alertThreshold: 20, clock: () => 0 }); const report = r.generateReport([entry("ok", 10), entry("bad", 30)]); expect(report.alerts).toHaveLength(1); expect(report.alerts[0]?.label).toBe("bad"); }); it("formatReport produces stable text", { timeout: 5000 }, () => { const r = new ProfileReporter({ clock: () => 0, alertThreshold: 1 }); const report = r.generateReport([entry("slow", 10)]); const text = r.formatReport(report); expect(text).toContain("Profile Report"); expect(text).toContain("Alerts (1)"); expect(text).toContain("slow"); }); it("formatReport with no alerts skips alerts section", { timeout: 5000, }, () => { const r = new ProfileReporter({ clock: () => 0 }); const text = r.formatReport(r.generateReport([entry("ok", 10)])); expect(text).not.toContain("Alerts"); }); it("computePercentile handles single-element exact index", { timeout: 5000, }, () => { const r = new ProfileReporter(); const stats = r.computeStats([42]); expect(stats.median).toBe(42); }); it("ignores entries without duration", { timeout: 5000 }, () => { const r = new ProfileReporter(); const report = r.generateReport( [{ label: "still", startTime: 0 }, entry("ok", 10)], [{ label: "mark", timestamp: 1 }] as readonly ProfileMark[] ); expect(report.stats.count).toBe(1); expect(report.marks).toHaveLength(1); }); });