diff --git a/frontend/apps/finance/src/features/dashboard/__tests__/CumulativeSpendChart.test.tsx b/frontend/apps/finance/src/features/dashboard/__tests__/CumulativeSpendChart.test.tsx new file mode 100644 index 0000000..af786c1 --- /dev/null +++ b/frontend/apps/finance/src/features/dashboard/__tests__/CumulativeSpendChart.test.tsx @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { + CumulativeSpendChart, +} from "../components/widgets/CumulativeSpendChart"; +import { + tooltipFormatter, + tooltipLabelFormatter, +} from "../components/widgets/cumulative-spend-chart-utils"; + +// ResponsiveContainer won't render children without real DOM dimensions. +// Mock it to render children directly so chart internals execute. +vi.mock("recharts", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; +}); + +// Data that produces a crossover: actual starts above ideal, then drops below +const testData = [ + { day: 1, actual: 20000, ideal: 10000 }, + { day: 2, actual: 35000, ideal: 20000 }, + { day: 3, actual: 40000, ideal: 30000 }, + { day: 4, actual: 42000, ideal: 40000 }, + { day: 5, actual: 44000, ideal: 50000 }, + { day: 6, actual: 46000, ideal: 60000 }, + { day: 7, actual: 48000, ideal: 70000 }, +]; + +describe("CumulativeSpendChart", () => { + it("renders chart card with title", () => { + render(); + expect(screen.getByText("Cumulative Spending")).toBeInTheDocument(); + }); + + it("renders with empty data without crashing", () => { + render(); + expect(screen.getByText("Cumulative Spending")).toBeInTheDocument(); + }); +}); + +describe("tooltipFormatter", () => { + it("returns null for array values (range area tuples)", () => { + expect(tooltipFormatter([100, 200], "surplus", "GBP")).toBeNull(); + }); + + it("formats scalar values as currency", () => { + const result = tooltipFormatter(150, "Actual", "GBP"); + expect(result).not.toBeNull(); + expect(result![0]).toContain("£"); + expect(result![1]).toBe("Actual"); + }); +}); + +describe("tooltipLabelFormatter", () => { + it("formats integer day labels", () => { + expect(tooltipLabelFormatter(5)).toBe("Day 5"); + expect(tooltipLabelFormatter(17)).toBe("Day 17"); + }); + + it("returns empty string for fractional crossover values", () => { + expect(tooltipLabelFormatter(17.16)).toBe(""); + expect(tooltipLabelFormatter(4.5)).toBe(""); + }); +}); diff --git a/frontend/apps/finance/src/features/dashboard/components/widgets/CumulativeSpendChart.tsx b/frontend/apps/finance/src/features/dashboard/components/widgets/CumulativeSpendChart.tsx index e10b8ba..cd16ba6 100644 --- a/frontend/apps/finance/src/features/dashboard/components/widgets/CumulativeSpendChart.tsx +++ b/frontend/apps/finance/src/features/dashboard/components/widgets/CumulativeSpendChart.tsx @@ -1,6 +1,10 @@ -import { formatCurrency, getCurrencySymbol } from "@gofin/core"; +import { getCurrencySymbol } from "@gofin/core"; import type { CumulativeSpendPoint } from "../../../../types"; import { insertCrossoverPoints } from "../../../../lib/insertCrossoverPoints"; +import { + tooltipFormatter, + tooltipLabelFormatter, +} from "./cumulative-spend-chart-utils"; import { Card, CardContent, @@ -40,13 +44,13 @@ export function CumulativeSpendChart({ data, currency }: CumulativeSpendChartPro const underBudget = point.actual <= point.ideal; if (point.isCrossover) { - // At crossover, define both areas to terminate/start seamlessly + // At crossover, both areas collapse to zero height for seamless transition return { day: point.day, actual: point.actual, ideal: point.ideal, - surplusTop: point.actual, - deficitTop: point.actual, + surplus: [point.actual, point.actual] as [number, number], + deficit: [point.actual, point.actual] as [number, number], }; } @@ -54,8 +58,12 @@ export function CumulativeSpendChart({ data, currency }: CumulativeSpendChartPro day: point.day, actual: point.actual, ideal: point.ideal, - surplusTop: underBudget ? point.ideal : undefined, - deficitTop: !underBudget ? point.actual : undefined, + surplus: underBudget + ? ([point.actual, point.ideal] as [number, number]) + : undefined, + deficit: !underBudget + ? ([point.ideal, point.actual] as [number, number]) + : undefined, }; }); @@ -77,17 +85,22 @@ export function CumulativeSpendChart({ data, currency }: CumulativeSpendChartPro p.day)} + allowDecimals={false} label={{ value: "Day of Month", position: "insideBottom", offset: -5 }} /> `${getCurrencySymbol(currency)}${value}`} /> formatCurrency((value as number) * 100, currency)} + formatter={(value, name) => tooltipFormatter(value, name as string, currency)} + labelFormatter={(label) => tooltipLabelFormatter(label)} />