date-fns-hijri/date-fns-hijri.test.ts
Aric Camarata d12117f000 fix: local-day adapter semantics with exact round-trips on hijri-core's UTC-day contract
Behavior changes (lock-step with hijri-core fix/utc-day-boundary):

- toHijriDate and all field/format/comparison/arithmetic functions now lift input
  Dates through localDayToUtcSlot() before calling coreToHijri(), reading the
  caller's LOCAL calendar day (date-fns convention). Previously passed the raw
  Date which caused off-by-one results in timezones west of UTC against the new
  UTC-day core contract.

- fromHijriDate now returns local-midnight Dates (new Date(y, m, d)) instead of
  UTC midnight. Local field accessors and date-fns format() render the intended
  calendar day on every host timezone. toISOString() is no longer the right API
  for this value.

- addHijriMonths, addHijriYears, startOfHijriMonth, endOfHijriMonth call
  fromHijriDate directly; the utcMidnightToLocalNoon shim is removed.

- Round-trip toHijriDate(fromHijriDate(y, m, d)) is now exact on every timezone.

Verified: 58/58 ESM tests, 10/10 CJS tests, 16/16 vitest assertions across
TZ=UTC, TZ=America/New_York, and TZ=Pacific/Auckland.
2026-06-10 16:38:32 -04:00

126 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Purpose: Vitest suite for date-fns-hijri — functional Hijri date utilities.
* Inputs: Pure functions from src/index.ts wrapping hijri-core. No network, no I/O.
* Outputs: Vitest pass/fail assertions.
* Constraints: UAQ range 13181500 AH; fromHijriDate throws on invalid input (null path).
* Use local-date constructor new Date(y, m, d) — not string "YYYY-MM-DD" which
* parses as UTC midnight and can be the previous LOCAL day west of UTC.
* Usage: pnpm vitest run
* SOT: packages.md — date-fns-hijri row
*/
import { describe, it, expect } from "vitest";
import {
toHijriDate,
fromHijriDate,
isValidHijriDate,
getHijriYear,
getHijriMonth,
getHijriDay,
getDaysInHijriMonth,
getHijriMonthName,
getHijriWeekdayName,
} from "./src/index";
// Anchor: 1 Ramadan 1446 = 2025-03-01 in the Gregorian calendar.
// Use local-date constructor to avoid the UTC-parsing pitfall with string form.
// At local noon the local calendar day is unambiguous on every timezone.
const RAMADAN_1446_NOON = new Date(2025, 2, 1, 12); // local noon 2025-03-01
describe("toHijriDate", () => {
it("converts noon 2025-03-01 UTC to 1 Ramadan 1446", () => {
const result = toHijriDate(RAMADAN_1446_NOON);
expect(result).not.toBeNull();
expect(result!.hy).toBe(1446);
expect(result!.hm).toBe(9);
expect(result!.hd).toBe(1);
});
it("returns null for dates outside UAQ range (2100)", () => {
expect(toHijriDate(new Date("2100-01-01"))).toBeNull();
});
});
describe("fromHijriDate", () => {
it("converts 1 Ramadan 1446 to local 2025-03-01 (via local accessors)", () => {
const result = fromHijriDate(1446, 9, 1);
// Returns local midnight: local accessors show the intended calendar day
// on every host timezone. Do NOT use toISOString() — it shows UTC which
// will be the previous day in timezones west of UTC.
expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(2); // March
expect(result.getDate()).toBe(1);
});
it("round-trip: toHijriDate(fromHijriDate(1446, 9, 1)) === {1446, 9, 1}", () => {
const d = fromHijriDate(1446, 9, 1);
const h = toHijriDate(d);
expect(h).not.toBeNull();
expect(h!.hy).toBe(1446);
expect(h!.hm).toBe(9);
expect(h!.hd).toBe(1);
});
it("throws on an out-of-range Hijri year (1501)", () => {
expect(() => fromHijriDate(1501, 1, 1)).toThrow();
});
});
describe("isValidHijriDate", () => {
it("returns true for 1 Ramadan 1446", () => {
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
});
it("returns false for month 13", () => {
expect(isValidHijriDate(1446, 13, 1)).toBe(false);
});
});
describe("field getters", () => {
it("getHijriYear returns 1446 for noon 2025-03-01", () => {
expect(getHijriYear(RAMADAN_1446_NOON)).toBe(1446);
});
it("getHijriMonth returns 9 for Ramadan", () => {
expect(getHijriMonth(RAMADAN_1446_NOON)).toBe(9);
});
it("getHijriDay returns 1", () => {
expect(getHijriDay(RAMADAN_1446_NOON)).toBe(1);
});
});
describe("getDaysInHijriMonth", () => {
it("returns 29 or 30 for Ramadan 1446", () => {
const days = getDaysInHijriMonth(1446, 9);
expect([29, 30]).toContain(days);
});
});
describe("getHijriMonthName", () => {
it("returns Ramadan for month 9 (long)", () => {
expect(getHijriMonthName(9, "long")).toBe("Ramadan");
});
it("throws RangeError for month 0", () => {
expect(() => getHijriMonthName(0)).toThrow(RangeError);
});
it("returns a non-empty medium name for month 1", () => {
const name = getHijriMonthName(1, "medium");
expect(name.length).toBeGreaterThan(0);
});
});
describe("getHijriWeekdayName", () => {
it("returns a non-empty long weekday name for 2025-03-01 (Saturday)", () => {
const name = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
expect(typeof name).toBe("string");
expect(name.length).toBeGreaterThan(0);
});
it("short name is no longer than long name for the same date", () => {
const long = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
const short = getHijriWeekdayName(RAMADAN_1446_NOON, "short");
expect(short.length).toBeLessThanOrEqual(long.length);
});
});