summarum/src/engine/datetime.test.ts
google-labs-jules[bot] a3341f2d7d test: resolve addToDate test flakiness across timezones
Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
2026-10-07 03:59:38 +00:00

229 lines
8 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import {
detectDateOrder,
resolveZone,
startOfToday,
addToDate,
isCalendarUnit,
} from "./datetime";
describe("datetime", () => {
describe("detectDateOrder", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("should detect dmy (e.g. en-GB)", () => {
expect(detectDateOrder("en-GB")).toBe("dmy");
});
it("should detect mdy (e.g. en-US)", () => {
expect(detectDateOrder("en-US")).toBe("mdy");
});
it("should default to dmy if neither is found", () => {
const mockFormatToParts = vi.fn().mockReturnValue([
{ type: "year", value: "2000" },
]);
const mockDateTimeFormat = vi.spyOn(Intl, "DateTimeFormat").mockImplementation(() => ({
formatToParts: mockFormatToParts,
}) as any);
expect(detectDateOrder()).toBe("dmy");
mockDateTimeFormat.mockRestore();
});
});
describe("resolveZone", () => {
it("should resolve 'local' to system timezone", () => {
const mockResolvedOptions = vi.fn().mockReturnValue({ timeZone: "America/Los_Angeles" });
const mockDateTimeFormat = vi.spyOn(Intl, "DateTimeFormat").mockImplementation(() => ({
resolvedOptions: mockResolvedOptions,
}) as any);
expect(resolveZone(["local"])).toBe("America/Los_Angeles");
mockDateTimeFormat.mockRestore();
});
it("should resolve known abbreviations", () => {
expect(resolveZone(["utc"])).toBe("UTC");
expect(resolveZone(["est"])).toBe("America/New_York");
expect(resolveZone(["pst"])).toBe("America/Los_Angeles");
expect(resolveZone(["jst"])).toBe("Asia/Tokyo");
});
it("should resolve known cities", () => {
expect(resolveZone(["new", "york"])).toBe("America/New_York");
expect(resolveZone(["london"])).toBe("Europe/London");
expect(resolveZone(["tokyo"])).toBe("Asia/Tokyo");
expect(resolveZone(["buenos", "aires"])).toBe("America/Argentina/Buenos_Aires");
});
it("should resolve russian cities", () => {
expect(resolveZone(["нью", "йорк"])).toBe("America/New_York");
expect(resolveZone(["москва"])).toBe("Europe/Moscow");
});
it("should ignore punctuation and case", () => {
expect(resolveZone(["New-York"])).toBe("America/New_York");
expect(resolveZone(["LONdoN."])).toBe("Europe/London");
});
it("should resolve raw IANA timezone", () => {
expect(resolveZone(["Europe", "Berlin"])).toBe("Europe/Berlin");
expect(resolveZone(["America", "Argentina", "Mendoza"])).toBe("America/Argentina/Mendoza");
});
it("should return null for unknown timezones", () => {
expect(resolveZone(["Unknown", "City"])).toBeNull();
expect(resolveZone(["Invalid/Timezone"])).toBeNull();
});
});
describe("startOfToday", () => {
afterEach(() => {
vi.useRealTimers();
});
it("should return midnight of the current day", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-05-15T14:30:00Z"));
const expected = new Date("2024-05-15T14:30:00Z");
expected.setHours(0, 0, 0, 0);
expect(startOfToday()).toBe(expected.getTime());
});
});
describe("addToDate", () => {
it("should add days", () => {
const ms = new Date(2024, 0, 1, 12, 0, 0).getTime(); // local Jan 1, 12:00
const res = new Date(addToDate(ms, 5, "day"));
expect(res.getFullYear()).toBe(2024);
expect(res.getMonth()).toBe(0);
expect(res.getDate()).toBe(6);
expect(res.getHours()).toBe(12);
});
it("should add weeks", () => {
const ms = new Date(2024, 0, 1, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 2, "week"));
expect(res.getFullYear()).toBe(2024);
expect(res.getMonth()).toBe(0);
expect(res.getDate()).toBe(15);
expect(res.getHours()).toBe(12);
});
it("should add months", () => {
const ms = new Date(2024, 0, 15, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 2, "month"));
expect(res.getFullYear()).toBe(2024);
expect(res.getMonth()).toBe(2); // March
expect(res.getDate()).toBe(15);
expect(res.getHours()).toBe(12);
});
it("should clamp end of month correctly", () => {
const ms = new Date(2024, 0, 31, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 1, "month"));
// 2024 is a leap year, so Feb 29
expect(res.getFullYear()).toBe(2024);
expect(res.getMonth()).toBe(1);
expect(res.getDate()).toBe(29);
const ms2 = new Date(2023, 0, 31, 12, 0, 0).getTime();
const res2 = new Date(addToDate(ms2, 1, "month"));
// 2023 is not a leap year, so Feb 28
expect(res2.getFullYear()).toBe(2023);
expect(res2.getMonth()).toBe(1);
expect(res2.getDate()).toBe(28);
});
it("should add years", () => {
const ms = new Date(2024, 0, 15, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 3, "year"));
expect(res.getFullYear()).toBe(2027);
expect(res.getMonth()).toBe(0);
expect(res.getDate()).toBe(15);
expect(res.getHours()).toBe(12);
});
it("should clamp end of month when adding years to leap day", () => {
const ms = new Date(2024, 1, 29, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 1, "year"));
expect(res.getFullYear()).toBe(2025);
expect(res.getMonth()).toBe(1);
expect(res.getDate()).toBe(28);
});
it("should handle unit ids with prefixes", () => {
const ms = new Date(2024, 0, 1, 12, 0, 0).getTime();
const res = new Date(addToDate(ms, 5, "unit:day"));
expect(res.getFullYear()).toBe(2024);
expect(res.getMonth()).toBe(0);
expect(res.getDate()).toBe(6);
});
it("should return original ms if amount is not an integer", () => {
const ms = new Date(2024, 0, 1, 12, 0, 0).getTime();
const res = addToDate(ms, 1.5, "day");
expect(res).toBe(ms);
});
it("should return original ms for unhandled units", () => {
const ms = new Date(2024, 0, 1, 12, 0, 0).getTime();
const res = addToDate(ms, 1, "hour");
expect(res).toBe(ms);
});
});
describe("isCalendarUnit", () => {
it("should return true for valid calendar units with integer amounts", () => {
expect(isCalendarUnit("day", 1)).toBe(true);
expect(isCalendarUnit("week", 5)).toBe(true);
expect(isCalendarUnit("month", -2)).toBe(true);
expect(isCalendarUnit("year", 10)).toBe(true);
expect(isCalendarUnit("unit:day", 1)).toBe(true);
});
it("should return false for floating point amounts", () => {
expect(isCalendarUnit("day", 1.5)).toBe(false);
expect(isCalendarUnit("month", 0.5)).toBe(false);
});
it("should return false for non-calendar units", () => {
expect(isCalendarUnit("hour", 1)).toBe(false);
expect(isCalendarUnit("minute", 1)).toBe(false);
expect(isCalendarUnit("unit:meter", 1)).toBe(false);
});
it("should handle edge cases for amount", () => {
expect(isCalendarUnit("day", 0)).toBe(true);
expect(isCalendarUnit("day", Number.MAX_SAFE_INTEGER)).toBe(true);
expect(isCalendarUnit("day", Number.MIN_SAFE_INTEGER)).toBe(true);
expect(isCalendarUnit("day", NaN)).toBe(false);
expect(isCalendarUnit("day", Infinity)).toBe(false);
expect(isCalendarUnit("day", -Infinity)).toBe(false);
});
it("should handle edge cases for unitId string format", () => {
expect(isCalendarUnit("", 1)).toBe(false);
expect(isCalendarUnit(":", 1)).toBe(false);
// string.split(":") behavior:
expect(isCalendarUnit("time:month", 1)).toBe(true);
expect(isCalendarUnit(":day", 1)).toBe(true);
expect(isCalendarUnit("day:", 1)).toBe(false);
expect(isCalendarUnit("a:day:c", 1)).toBe(true);
});
it("should return false for case-sensitive mismatch", () => {
expect(isCalendarUnit("Day", 1)).toBe(false);
expect(isCalendarUnit("YEAR", 1)).toBe(false);
expect(isCalendarUnit("MONTH", 1)).toBe(false);
});
});
});