blob: fccac7a17bb200a3e98d61227ccada87e3210756 [file]
import { describe, expect, it } from "vitest";
import {
MONTH_NAMES,
addDays,
addMonths,
addYears,
dayKey,
daysInMonth,
endOfDay,
formatDate,
formatMonthYear,
formatTime,
formatViewTitle,
fromDayKey,
isSameDay,
isToday,
isWeekend,
monthGrid,
parseISO,
rangeForView,
startOfDay,
startOfMonth,
startOfWeek,
startOfYear,
step,
toDateInput,
toISO,
toLocalInput,
weekDays,
yearMonths,
} from "./dates";
const MARCH_10_2026 = new Date(2026, 2, 10, 14, 37, 12); // a Tuesday
describe("boundaries", () => {
it("startOfDay strips the time", () => {
const start = startOfDay(MARCH_10_2026);
expect(start.getHours()).toBe(0);
expect(start.getMinutes()).toBe(0);
expect(start.getDate()).toBe(10);
});
it("endOfDay is midnight the next day", () => {
expect(endOfDay(MARCH_10_2026).getTime() - startOfDay(MARCH_10_2026).getTime()).toBe(86_400_000);
});
it("startOfWeek finds the Monday", () => {
const monday = startOfWeek(MARCH_10_2026);
expect(monday.getDay()).toBe(1);
expect(monday.getDate()).toBe(9);
});
it("startOfWeek on a Sunday goes back six days", () => {
const sunday = new Date(2026, 2, 15);
expect(startOfWeek(sunday).getDate()).toBe(9);
});
it("startOfWeek can start on Sunday instead", () => {
expect(startOfWeek(MARCH_10_2026, 0).getDay()).toBe(0);
});
it("startOfMonth and startOfYear", () => {
expect(startOfMonth(MARCH_10_2026).getDate()).toBe(1);
expect(startOfYear(MARCH_10_2026).getMonth()).toBe(0);
expect(startOfYear(MARCH_10_2026).getDate()).toBe(1);
});
});
describe("arithmetic", () => {
it("addDays crosses month boundaries", () => {
const result = addDays(new Date(2026, 0, 31), 1);
expect(result.getMonth()).toBe(1);
expect(result.getDate()).toBe(1);
});
it("addDays goes backwards", () => {
expect(addDays(new Date(2026, 0, 1), -1).getFullYear()).toBe(2025);
});
it("addMonths clamps a day that does not exist in the target month", () => {
const result = addMonths(new Date(2026, 0, 31), 1);
expect(result.getMonth()).toBe(1);
expect(result.getDate()).toBe(28);
});
it("addMonths handles a leap February", () => {
expect(addMonths(new Date(2028, 0, 31), 1).getDate()).toBe(29);
});
it("addYears moves twelve months", () => {
expect(addYears(MARCH_10_2026, 1).getFullYear()).toBe(2027);
});
it("daysInMonth", () => {
expect(daysInMonth(2026, 1)).toBe(28);
expect(daysInMonth(2028, 1)).toBe(29);
expect(daysInMonth(2026, 0)).toBe(31);
});
});
describe("comparisons", () => {
it("isSameDay ignores the time", () => {
expect(isSameDay(new Date(2026, 2, 10, 1), new Date(2026, 2, 10, 23))).toBe(true);
expect(isSameDay(new Date(2026, 2, 10), new Date(2026, 2, 11))).toBe(false);
});
it("isSameDay is not fooled by the same day in another year", () => {
expect(isSameDay(new Date(2026, 2, 10), new Date(2027, 2, 10))).toBe(false);
});
it("isToday takes an injectable now", () => {
expect(isToday(new Date(2026, 2, 10), new Date(2026, 2, 10, 18))).toBe(true);
expect(isToday(new Date(2026, 2, 11), new Date(2026, 2, 10))).toBe(false);
});
it("isWeekend", () => {
expect(isWeekend(new Date(2026, 2, 14))).toBe(true); // Saturday
expect(isWeekend(new Date(2026, 2, 15))).toBe(true); // Sunday
expect(isWeekend(new Date(2026, 2, 16))).toBe(false);
});
});
describe("grids", () => {
it("a month grid is always 42 days", () => {
for (let month = 0; month < 12; month += 1) {
expect(monthGrid(new Date(2026, month, 1))).toHaveLength(42);
}
});
it("a month grid starts on a Monday and contains the whole month", () => {
const grid = monthGrid(MARCH_10_2026);
expect(grid[0].getDay()).toBe(1);
const marchDays = grid.filter((day) => day.getMonth() === 2);
expect(marchDays).toHaveLength(31);
});
it("a month grid is contiguous", () => {
const grid = monthGrid(MARCH_10_2026);
for (let index = 1; index < grid.length; index += 1) {
expect(grid[index].getTime() - grid[index - 1].getTime()).toBe(86_400_000);
}
});
it("weekDays returns Monday to Sunday", () => {
const days = weekDays(MARCH_10_2026);
expect(days).toHaveLength(7);
expect(days[0].getDay()).toBe(1);
expect(days[6].getDay()).toBe(0);
});
it("yearMonths returns the twelve firsts", () => {
const months = yearMonths(MARCH_10_2026);
expect(months).toHaveLength(12);
expect(months.every((month) => month.getDate() === 1)).toBe(true);
expect(months.map((month) => month.getMonth())).toEqual([...Array(12).keys()]);
});
});
describe("view ranges", () => {
it("day", () => {
const range = rangeForView("day", MARCH_10_2026);
expect(range.end.getTime() - range.start.getTime()).toBe(86_400_000);
});
it("week covers seven days from Monday", () => {
const range = rangeForView("week", MARCH_10_2026);
expect(range.start.getDay()).toBe(1);
expect(range.end.getTime() - range.start.getTime()).toBe(7 * 86_400_000);
});
it("month covers the whole 42-day grid, not just the calendar month", () => {
const range = rangeForView("month", MARCH_10_2026);
const grid = monthGrid(MARCH_10_2026);
expect(range.start.getTime()).toBe(grid[0].getTime());
expect(range.end.getTime()).toBe(addDays(grid[41], 1).getTime());
});
it("year covers January to January", () => {
const range = rangeForView("year", MARCH_10_2026);
expect(range.start.getFullYear()).toBe(2026);
expect(range.end.getFullYear()).toBe(2027);
expect(range.end.getMonth()).toBe(0);
});
it("agenda looks forward 90 days", () => {
const range = rangeForView("agenda", MARCH_10_2026);
expect(range.end.getTime() - range.start.getTime()).toBe(90 * 86_400_000);
});
it("an unknown view falls back to month", () => {
expect(rangeForView("wat", MARCH_10_2026)).toEqual(rangeForView("month", MARCH_10_2026));
});
});
describe("stepping", () => {
it("moves by one view-sized unit", () => {
expect(step("day", MARCH_10_2026, 1).getDate()).toBe(11);
expect(step("week", MARCH_10_2026, 1).getDate()).toBe(17);
expect(step("month", MARCH_10_2026, 1).getMonth()).toBe(3);
expect(step("year", MARCH_10_2026, 1).getFullYear()).toBe(2027);
expect(step("agenda", MARCH_10_2026, 1).getMonth()).toBe(3);
});
it("moves backwards too", () => {
expect(step("month", MARCH_10_2026, -1).getMonth()).toBe(1);
expect(step("day", MARCH_10_2026, -1).getDate()).toBe(9);
});
it("stepping forwards then back returns to the start", () => {
for (const view of ["day", "week", "month", "year"]) {
const there = step(view, MARCH_10_2026, 1);
expect(step(view, there, -1).getTime()).toBe(MARCH_10_2026.getTime());
}
});
});
describe("keys and formatting", () => {
it("dayKey is zero padded", () => {
expect(dayKey(new Date(2026, 0, 5))).toBe("2026-01-05");
expect(dayKey(new Date(2026, 11, 31))).toBe("2026-12-31");
});
it("dayKey round-trips through fromDayKey", () => {
const date = new Date(2026, 6, 4);
expect(fromDayKey(dayKey(date)).getTime()).toBe(date.getTime());
});
it("formatTime pads", () => {
expect(formatTime(new Date(2026, 2, 10, 9, 5))).toBe("09:05");
expect(formatTime(new Date(2026, 2, 10, 23, 59))).toBe("23:59");
});
it("formatDate and formatMonthYear", () => {
expect(formatDate(new Date(2026, 2, 10))).toBe("10 March 2026");
expect(formatMonthYear(new Date(2026, 2, 10))).toBe("March 2026");
});
it("MONTH_NAMES has twelve entries", () => {
expect(MONTH_NAMES).toHaveLength(12);
});
it("view titles", () => {
expect(formatViewTitle("month", MARCH_10_2026)).toBe("March 2026");
expect(formatViewTitle("year", MARCH_10_2026)).toBe("2026");
expect(formatViewTitle("day", MARCH_10_2026)).toBe("10 March 2026");
expect(formatViewTitle("agenda", MARCH_10_2026)).toBe("From 10 March 2026");
});
it("a week title inside one month is compact", () => {
expect(formatViewTitle("week", MARCH_10_2026)).toBe("9 - 15 March 2026");
});
it("a week title spanning two months names both", () => {
expect(formatViewTitle("week", new Date(2026, 2, 31))).toBe("30 March - 5 April 2026");
});
it("a week title spanning two years names both in full", () => {
expect(formatViewTitle("week", new Date(2026, 11, 31))).toBe("28 December 2026 - 3 January 2027");
});
it("input formatting", () => {
const date = new Date(2026, 2, 10, 9, 5);
expect(toLocalInput(date)).toBe("2026-03-10T09:05");
expect(toDateInput(date)).toBe("2026-03-10");
});
it("ISO round trip", () => {
const iso = "2026-03-10T09:00:00Z";
expect(toISO(parseISO(iso))).toBe(iso);
});
it("toISO drops milliseconds", () => {
expect(toISO(new Date(Date.UTC(2026, 2, 10, 9, 0, 0, 123)))).toBe("2026-03-10T09:00:00Z");
});
});