diff --git a/lib/bills/urgency.test.ts b/lib/bills/urgency.test.ts new file mode 100644 index 0000000..72b5012 --- /dev/null +++ b/lib/bills/urgency.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { daysDiff, computeUrgency, computeDaysInfo } from "./urgency"; + +describe("Bills Urgency", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("daysDiff", () => { + it("returns 0 for today", () => { + vi.setSystemTime(new Date(2024, 0, 15, 12, 0, 0)); + expect(daysDiff("2024-01-15T00:00:00")).toBe(0); + expect(daysDiff("2024-01-15T23:59:59")).toBe(0); + }); + + it("returns negative for past dates (overdue)", () => { + vi.setSystemTime(new Date(2024, 0, 15, 12, 0, 0)); + expect(daysDiff("2024-01-14T10:00:00")).toBe(-1); + expect(daysDiff("2024-01-01T00:00:00")).toBe(-14); + }); + + it("returns positive for future dates (upcoming)", () => { + vi.setSystemTime(new Date(2024, 0, 15, 12, 0, 0)); + expect(daysDiff("2024-01-16T10:00:00")).toBe(1); + expect(daysDiff("2024-01-20T00:00:00")).toBe(5); + }); + + it("handles end-of-month rollover", () => { + vi.setSystemTime(new Date(2024, 0, 31, 12, 0, 0)); + expect(daysDiff("2024-02-01T00:00:00")).toBe(1); + }); + + it("handles leap year boundaries", () => { + vi.setSystemTime(new Date(2024, 1, 28, 12, 0, 0)); + expect(daysDiff("2024-02-29T00:00:00")).toBe(1); + expect(daysDiff("2024-03-01T00:00:00")).toBe(2); + + // non-leap year + vi.setSystemTime(new Date(2023, 1, 28, 12, 0, 0)); + expect(daysDiff("2023-03-01T00:00:00")).toBe(1); + }); + + it("ignores timezone offset differences by standardizing calendar days", () => { + vi.setSystemTime(new Date(2024, 0, 15, 23, 0, 0)); + expect(daysDiff("2024-01-15T01:00:00")).toBe(0); + expect(daysDiff("2024-01-16T01:00:00")).toBe(1); + }); + }); + + describe("computeUrgency", () => { + beforeEach(() => { + vi.setSystemTime(new Date(2024, 4, 15, 12, 0, 0)); + }); + + it("returns overdue for past dates", () => { + expect(computeUrgency("2024-05-14T00:00:00")).toBe("overdue"); + expect(computeUrgency("2024-05-01T00:00:00")).toBe("overdue"); + }); + + it("returns urgent for today and next 3 days", () => { + expect(computeUrgency("2024-05-15T00:00:00")).toBe("urgent"); // 0 + expect(computeUrgency("2024-05-16T00:00:00")).toBe("urgent"); // 1 + expect(computeUrgency("2024-05-17T00:00:00")).toBe("urgent"); // 2 + expect(computeUrgency("2024-05-18T00:00:00")).toBe("urgent"); // 3 + }); + + it("returns upcoming for 4 days and beyond", () => { + expect(computeUrgency("2024-05-19T00:00:00")).toBe("upcoming"); // 4 + expect(computeUrgency("2024-06-01T00:00:00")).toBe("upcoming"); // far future + }); + + it("asserts the urgent-upcoming boundary", () => { + expect(computeUrgency("2024-05-18T00:00:00")).toBe("urgent"); + expect(computeUrgency("2024-05-19T00:00:00")).toBe("upcoming"); + }); + }); + + describe("computeDaysInfo", () => { + beforeEach(() => { + vi.setSystemTime(new Date(2024, 4, 15, 12, 0, 0)); + }); + + it("formats overdue dates correctly", () => { + expect(computeDaysInfo("2024-05-14T00:00:00")).toBe("1d overdue"); + expect(computeDaysInfo("2024-05-05T00:00:00")).toBe("10d overdue"); + }); + + it("formats due today correctly", () => { + expect(computeDaysInfo("2024-05-15T00:00:00")).toBe("Due today"); + }); + + it("formats due tomorrow correctly", () => { + expect(computeDaysInfo("2024-05-16T00:00:00")).toBe("Due tomorrow"); + }); + + it("formats upcoming dates correctly", () => { + expect(computeDaysInfo("2024-05-17T00:00:00")).toBe("2d left"); + expect(computeDaysInfo("2024-05-25T00:00:00")).toBe("10d left"); + }); + }); +}); diff --git a/lib/bills/urgency.ts b/lib/bills/urgency.ts index 50a4369..63ab6ca 100644 --- a/lib/bills/urgency.ts +++ b/lib/bills/urgency.ts @@ -3,7 +3,7 @@ * Keeps mock data and API responses in sync with the same logic. */ -export type BillUrgency = 'overdue' | 'urgent' | 'upcoming' | 'paid'; +export type BillUrgency = "overdue" | "urgent" | "upcoming" | "paid"; /** Returns days difference: negative = overdue, 0 = today, positive = future */ export function daysDiff(dueDateStr: string): number { @@ -17,15 +17,15 @@ export function daysDiff(dueDateStr: string): number { export function computeUrgency(dueDateStr: string): BillUrgency { const diff = daysDiff(dueDateStr); - if (diff < 0) return 'overdue'; - if (diff <= 3) return 'urgent'; - return 'upcoming'; + if (diff < 0) return "overdue"; + if (diff <= 3) return "urgent"; + return "upcoming"; } export function computeDaysInfo(dueDateStr: string): string { const diff = daysDiff(dueDateStr); if (diff < 0) return `${Math.abs(diff)}d overdue`; - if (diff === 0) return 'Due today'; - if (diff === 1) return 'Due tomorrow'; + if (diff === 0) return "Due today"; + if (diff === 1) return "Due tomorrow"; return `${diff}d left`; } diff --git a/vitest.config.mts b/vitest.config.mts index 6b5c80d..75c0611 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -5,6 +5,7 @@ export default defineConfig({ test: { include: [ 'lib/contracts/**/*.test.ts', + 'lib/**/*.test.ts', 'tests/unit/**/*.test.ts', 'tests/unit/**/*.test.cjs', 'tests/integration/**/*.test.ts', @@ -22,6 +23,7 @@ export default defineConfig({ include: ['lib/contracts/**/*.ts', 'app/**/*.ts', 'lib/**/*.ts'], exclude: [ 'lib/contracts/**/*.test.ts', + 'lib/**/*.test.ts', 'tests/**', ], },