/** * Unit tests for dateUtils: localDateString, dutyOverlapsLocalDay, * dutyOverlapsLocalRange, getMonday, formatHHMM. */ import { describe, it, expect } from "vitest"; import { localDateString, dutyOverlapsLocalDay, dutyOverlapsLocalRange, getMonday, formatHHMM, firstDayOfMonth, lastDayOfMonth, formatDateKey, dateKeyToDDMM, } from "./dateUtils.js"; describe("localDateString", () => { it("formats date as YYYY-MM-DD", () => { const d = new Date(2025, 0, 15); expect(localDateString(d)).toBe("2025-01-15"); }); it("pads month and day with zero", () => { expect(localDateString(new Date(2025, 0, 5))).toBe("2025-01-05"); expect(localDateString(new Date(2025, 8, 9))).toBe("2025-09-09"); }); it("handles December and year boundary", () => { expect(localDateString(new Date(2024, 11, 31))).toBe("2024-12-31"); }); }); describe("dutyOverlapsLocalDay", () => { it("returns true when duty spans the whole day", () => { const d = { start_at: "2025-02-25T00:00:00Z", end_at: "2025-02-25T23:59:59Z", }; expect(dutyOverlapsLocalDay(d, "2025-02-25")).toBe(true); }); it("returns true when duty overlaps part of the day", () => { const d = { start_at: "2025-02-25T09:00:00Z", end_at: "2025-02-25T14:00:00Z", }; expect(dutyOverlapsLocalDay(d, "2025-02-25")).toBe(true); }); it("returns true when duty continues from previous day", () => { const d = { start_at: "2025-02-24T22:00:00Z", end_at: "2025-02-25T06:00:00Z", }; expect(dutyOverlapsLocalDay(d, "2025-02-25")).toBe(true); }); it("returns false when duty ends before the day", () => { const d = { start_at: "2025-02-24T09:00:00Z", end_at: "2025-02-24T18:00:00Z", }; expect(dutyOverlapsLocalDay(d, "2025-02-25")).toBe(false); }); it("returns false when duty starts after the day", () => { const d = { start_at: "2025-02-26T09:00:00Z", end_at: "2025-02-26T18:00:00Z", }; expect(dutyOverlapsLocalDay(d, "2025-02-25")).toBe(false); }); }); describe("dutyOverlapsLocalRange", () => { it("returns true when duty overlaps the range", () => { const d = { start_at: "2025-02-24T12:00:00Z", end_at: "2025-02-26T12:00:00Z", }; expect(dutyOverlapsLocalRange(d, "2025-02-25", "2025-02-28")).toBe(true); }); it("returns true when duty is entirely inside the range", () => { const d = { start_at: "2025-02-26T09:00:00Z", end_at: "2025-02-26T18:00:00Z", }; expect(dutyOverlapsLocalRange(d, "2025-02-25", "2025-02-28")).toBe(true); }); it("returns false when duty ends before range start", () => { const d = { start_at: "2025-02-20T09:00:00Z", end_at: "2025-02-22T18:00:00Z", }; expect(dutyOverlapsLocalRange(d, "2025-02-25", "2025-02-28")).toBe(false); }); it("returns false when duty starts after range end", () => { const d = { start_at: "2025-03-01T09:00:00Z", end_at: "2025-03-01T18:00:00Z", }; expect(dutyOverlapsLocalRange(d, "2025-02-25", "2025-02-28")).toBe(false); }); }); describe("getMonday", () => { it("returns same day when date is Monday", () => { const monday = new Date(2025, 0, 6); // 6 Jan 2025 is Monday const result = getMonday(monday); expect(result.getFullYear()).toBe(2025); expect(result.getMonth()).toBe(0); expect(result.getDate()).toBe(6); expect(result.getDay()).toBe(1); }); it("returns previous Monday for Wednesday", () => { const wed = new Date(2025, 0, 8); const result = getMonday(wed); expect(result.getDay()).toBe(1); expect(result.getDate()).toBe(6); }); it("returns Monday of same week for Sunday", () => { const sun = new Date(2025, 0, 12); const result = getMonday(sun); expect(result.getDay()).toBe(1); expect(result.getDate()).toBe(6); }); }); describe("formatHHMM", () => { it("formats ISO string as HH:MM in local time", () => { const s = "2025-02-25T14:30:00Z"; const result = formatHHMM(s); expect(result).toMatch(/^\d{2}:\d{2}$/); const d = new Date(s); const expected = (d.getHours() < 10 ? "0" : "") + d.getHours() + ":" + (d.getMinutes() < 10 ? "0" : "") + d.getMinutes(); expect(result).toBe(expected); }); it("returns empty string for null", () => { expect(formatHHMM(null)).toBe(""); }); it("returns empty string for undefined", () => { expect(formatHHMM(undefined)).toBe(""); }); it("returns empty string for empty string", () => { expect(formatHHMM("")).toBe(""); }); it("pads hours and minutes with zero", () => { const s = "2025-02-25T09:05:00Z"; const result = formatHHMM(s); expect(result).toMatch(/^\d{2}:\d{2}$/); }); }); describe("firstDayOfMonth", () => { it("returns first day of month", () => { const d = new Date(2025, 5, 15); const result = firstDayOfMonth(d); expect(result.getFullYear()).toBe(2025); expect(result.getMonth()).toBe(5); expect(result.getDate()).toBe(1); }); it("handles January", () => { const d = new Date(2025, 0, 31); const result = firstDayOfMonth(d); expect(result.getDate()).toBe(1); expect(result.getMonth()).toBe(0); }); }); describe("lastDayOfMonth", () => { it("returns last day of month", () => { const d = new Date(2025, 0, 15); const result = lastDayOfMonth(d); expect(result.getFullYear()).toBe(2025); expect(result.getMonth()).toBe(0); expect(result.getDate()).toBe(31); }); it("returns 28 for non-leap February", () => { const d = new Date(2023, 1, 1); const result = lastDayOfMonth(d); expect(result.getDate()).toBe(28); expect(result.getMonth()).toBe(1); }); it("returns 29 for leap February", () => { const d = new Date(2024, 1, 1); const result = lastDayOfMonth(d); expect(result.getDate()).toBe(29); }); }); describe("formatDateKey", () => { it("formats ISO date string as DD.MM (local time)", () => { const result = formatDateKey("2025-02-25T00:00:00Z"); expect(result).toMatch(/^\d{2}\.\d{2}$/); const [day, month] = result.split("."); expect(Number(day)).toBeGreaterThanOrEqual(1); expect(Number(day)).toBeLessThanOrEqual(31); expect(Number(month)).toBeGreaterThanOrEqual(1); expect(Number(month)).toBeLessThanOrEqual(12); }); it("returns DD.MM format with zero-padding", () => { const result = formatDateKey("2025-01-05T12:00:00Z"); expect(result).toMatch(/^\d{2}\.\d{2}$/); }); }); describe("dateKeyToDDMM", () => { it("converts YYYY-MM-DD to DD.MM", () => { expect(dateKeyToDDMM("2025-02-25")).toBe("25.02"); }); it("handles single-digit day and month", () => { expect(dateKeyToDDMM("2025-01-09")).toBe("09.01"); }); });