All checks were successful
PR checks / checks (pull_request) Successful in 31s
Overhaul the manager dashboard "Total Approved Spend" stat card per the reporter's request: - Swap the DollarSign lucide icon for IndianRupee (rupee symbol). - Render the amount in the Indian short scale (lakh/crore) via a new `formatCompactINR` helper, e.g. ₹2 Cr, ₹49 L, ₹75 K, instead of the full ₹49,00,000.00. `formatCompactINR` rounds to at most 2 decimals, trims trailing zeros, keeps the ₹ prefix and sign. The DollarSign icon is retained for the Accounts "Payment Queue Value" card; the precise `formatCurrency` is kept for tables. Adds unit tests covering crore/lakh/thousand/sub-thousand, boundaries, zero, string input, negatives and non-finite input. Fixes #50 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
4.6 KiB
TypeScript
158 lines
4.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
formatCurrency, formatCompactINR, formatDate, formatDateTime,
|
|
generatePoNumber, PO_STATUS_LABELS, PO_STATUS_VARIANTS,
|
|
} from "@/lib/utils";
|
|
|
|
describe("formatCurrency", () => {
|
|
it("formats numbers as INR by default", () => {
|
|
const result = formatCurrency(1000);
|
|
expect(result).toMatch(/1,000/);
|
|
expect(result).toMatch(/₹|INR/);
|
|
});
|
|
|
|
it("formats decimal amounts to 2 decimal places", () => {
|
|
const result = formatCurrency(1234.5);
|
|
expect(result).toMatch(/1,234/);
|
|
});
|
|
|
|
it("accepts string input and formats it", () => {
|
|
const result = formatCurrency("500");
|
|
expect(result).toMatch(/500/);
|
|
});
|
|
|
|
it("formats zero without error", () => {
|
|
const result = formatCurrency(0);
|
|
expect(result).toMatch(/0/);
|
|
});
|
|
|
|
it("formats large numbers with commas", () => {
|
|
const result = formatCurrency(225498);
|
|
expect(result).toMatch(/2,25,498|225,498/); // en-IN uses 2,25,498 grouping
|
|
});
|
|
});
|
|
|
|
describe("formatCompactINR", () => {
|
|
it("abbreviates crore amounts with Cr", () => {
|
|
expect(formatCompactINR(20000000)).toBe("₹2 Cr");
|
|
});
|
|
|
|
it("abbreviates lakh amounts with L", () => {
|
|
expect(formatCompactINR(4900000)).toBe("₹49 L");
|
|
});
|
|
|
|
it("abbreviates thousand amounts with K", () => {
|
|
expect(formatCompactINR(75000)).toBe("₹75 K");
|
|
});
|
|
|
|
it("renders sub-thousand amounts without a suffix", () => {
|
|
expect(formatCompactINR(500)).toBe("₹500");
|
|
});
|
|
|
|
it("formats zero as ₹0", () => {
|
|
expect(formatCompactINR(0)).toBe("₹0");
|
|
});
|
|
|
|
it("trims trailing zeros but keeps significant decimals", () => {
|
|
expect(formatCompactINR(25000000)).toBe("₹2.5 Cr");
|
|
expect(formatCompactINR(4950000)).toBe("₹49.5 L");
|
|
});
|
|
|
|
it("rounds to at most two decimals", () => {
|
|
expect(formatCompactINR(12345678)).toBe("₹1.23 Cr");
|
|
});
|
|
|
|
it("uses the right unit at boundaries", () => {
|
|
expect(formatCompactINR(100000)).toBe("₹1 L");
|
|
expect(formatCompactINR(10000000)).toBe("₹1 Cr");
|
|
expect(formatCompactINR(1000)).toBe("₹1 K");
|
|
});
|
|
|
|
it("accepts string input", () => {
|
|
expect(formatCompactINR("4900000")).toBe("₹49 L");
|
|
});
|
|
|
|
it("preserves the sign for negative amounts", () => {
|
|
expect(formatCompactINR(-4900000)).toBe("-₹49 L");
|
|
});
|
|
|
|
it("handles non-finite input gracefully", () => {
|
|
expect(formatCompactINR(NaN)).toBe("₹0");
|
|
});
|
|
});
|
|
|
|
describe("formatDate", () => {
|
|
it("returns a readable date string", () => {
|
|
const result = formatDate(new Date("2026-04-29"));
|
|
expect(result).toMatch(/2026/);
|
|
expect(result).toMatch(/Apr|29/);
|
|
});
|
|
|
|
it("accepts a date string as input", () => {
|
|
const result = formatDate("2026-01-15");
|
|
expect(result).toMatch(/2026/);
|
|
});
|
|
});
|
|
|
|
describe("formatDateTime", () => {
|
|
it("includes both date and time", () => {
|
|
const result = formatDateTime(new Date("2026-04-29T14:30:00"));
|
|
expect(result).toMatch(/2026/);
|
|
// Should contain hours
|
|
expect(result).toMatch(/\d{1,2}:\d{2}/);
|
|
});
|
|
});
|
|
|
|
describe("generatePoNumber", () => {
|
|
it("starts with PO-", () => {
|
|
expect(generatePoNumber()).toMatch(/^PO-/);
|
|
});
|
|
|
|
it("includes the current year", () => {
|
|
const year = new Date().getFullYear().toString();
|
|
expect(generatePoNumber()).toContain(year);
|
|
});
|
|
|
|
it("generates a 5-digit zero-padded sequence", () => {
|
|
const result = generatePoNumber();
|
|
// Format: PO-YYYY-NNNNN
|
|
expect(result).toMatch(/^PO-\d{4}-\d{5}$/);
|
|
});
|
|
|
|
it("generates unique values across calls", () => {
|
|
const numbers = new Set(Array.from({ length: 20 }, () => generatePoNumber()));
|
|
// Very unlikely to get a collision with 20 draws from 100000
|
|
expect(numbers.size).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe("PO_STATUS_LABELS", () => {
|
|
it("maps every status to a non-empty label", () => {
|
|
const statuses = [
|
|
"DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING",
|
|
"EDITS_REQUESTED", "REJECTED", "MGR_APPROVED",
|
|
"SENT_FOR_PAYMENT", "PAID_DELIVERED", "CLOSED",
|
|
] as const;
|
|
for (const s of statuses) {
|
|
expect(PO_STATUS_LABELS[s]).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("PO_STATUS_VARIANTS", () => {
|
|
it("assigns danger variant to REJECTED", () => {
|
|
expect(PO_STATUS_VARIANTS["REJECTED"]).toBe("danger");
|
|
});
|
|
|
|
it("assigns success variant to MGR_APPROVED", () => {
|
|
expect(PO_STATUS_VARIANTS["MGR_APPROVED"]).toBe("success");
|
|
});
|
|
|
|
it("assigns warning variant to EDITS_REQUESTED", () => {
|
|
expect(PO_STATUS_VARIANTS["EDITS_REQUESTED"]).toBe("warning");
|
|
});
|
|
|
|
it("assigns outline variant to DRAFT", () => {
|
|
expect(PO_STATUS_VARIANTS["DRAFT"]).toBe("outline");
|
|
});
|
|
});
|