- Create unified date/time formatter with Intl-first approach and dayjs fallback - Add support for is/lt/nb locales in Booker components - Unify date formatting across Booker (Header, dates, weekdays) - Update translation pipeline to include is, lt, nb - Add tests for new locales and formatter - Add timezone support in dayjs fallback - Add formatter caching with circuit breaker - Use unified formatDateTime in Header.tsx
342 lines
9.9 KiB
TypeScript
342 lines
9.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
|
||
import "@calcom/dayjs/locales";
|
||
|
||
import { formatDateTime, formatDateTimeRange, formatWeekday, getWeekdayNames } from "./dateTimeFormatter";
|
||
|
||
describe("dateTimeFormatter", () => {
|
||
describe("formatDateTime", () => {
|
||
describe("supported locales (Intl path)", () => {
|
||
it("formats full date style in English", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "en-US",
|
||
timeZone: "UTC",
|
||
dateStyle: "full",
|
||
});
|
||
|
||
// Validate actual content
|
||
expect(result).toContain("Monday");
|
||
expect(result).toContain("December");
|
||
expect(result).toContain("1");
|
||
expect(result).toContain("2025");
|
||
});
|
||
|
||
it("formats time with 12h format", () => {
|
||
const date = new Date("2025-12-01T14:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "en-US",
|
||
timeZone: "UTC",
|
||
timeStyle: "short",
|
||
hour12: true,
|
||
});
|
||
|
||
expect(result).toMatch(/2:30\s*PM/);
|
||
});
|
||
|
||
it("formats month only", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "en-US",
|
||
month: "long",
|
||
});
|
||
|
||
expect(result).toBe("December");
|
||
});
|
||
});
|
||
|
||
describe("unsupported locales (dayjs fallback)", () => {
|
||
it("formats Icelandic date correctly", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
dateStyle: "long",
|
||
});
|
||
|
||
// Validate Icelandic content
|
||
expect(result).toMatch(/desember/i); // December in Icelandic
|
||
expect(result).toContain("1");
|
||
expect(result).toContain("2025");
|
||
});
|
||
|
||
it("formats Lithuanian date correctly", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "lt",
|
||
dateStyle: "long",
|
||
});
|
||
|
||
// Validate Lithuanian content
|
||
expect(result).toMatch(/gruod/i); // December in Lithuanian
|
||
expect(result).toContain("1");
|
||
expect(result).toContain("2025");
|
||
});
|
||
|
||
it("formats Norwegian Bokmål date correctly", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "nb",
|
||
dateStyle: "long",
|
||
});
|
||
|
||
// Validate Norwegian content
|
||
expect(result).toMatch(/desember/i); // December in Norwegian
|
||
expect(result).toContain("1");
|
||
expect(result).toContain("2025");
|
||
});
|
||
|
||
it("formats month-only in Icelandic", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
month: "long",
|
||
});
|
||
|
||
expect(result).toMatch(/desember/i);
|
||
});
|
||
|
||
it("formats narrow month (first character)", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
month: "narrow",
|
||
});
|
||
|
||
expect(result).toHaveLength(1);
|
||
});
|
||
|
||
it("formats time with 12h format in Icelandic", () => {
|
||
const date = new Date("2025-12-01T14:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
timeStyle: "short",
|
||
hour12: true,
|
||
});
|
||
|
||
expect(result).toMatch(/2:30/);
|
||
expect(result).toMatch(/(PM|e\.h\.)/i); // English PM or Icelandic "eftir hádegi"
|
||
});
|
||
|
||
it("formats combined date and time", () => {
|
||
const date = new Date("2025-12-01T14:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
dateStyle: "medium",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
|
||
expect(result).toContain("1");
|
||
expect(result).toMatch(/14:30/);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("formatDateTimeRange", () => {
|
||
describe("supported locales", () => {
|
||
it("formats time range on same day", () => {
|
||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||
const endDate = new Date("2025-12-01T11:30:00Z");
|
||
|
||
const result = formatDateTimeRange(startDate, endDate, {
|
||
locale: "en-US",
|
||
timeZone: "UTC",
|
||
timeStyle: "short",
|
||
});
|
||
|
||
expect(result).toMatch(/10:30/);
|
||
expect(result).toMatch(/11:30/);
|
||
});
|
||
|
||
it("formats date range across different days", () => {
|
||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||
const endDate = new Date("2025-12-02T11:30:00Z");
|
||
|
||
const result = formatDateTimeRange(startDate, endDate, {
|
||
locale: "en-US",
|
||
timeZone: "UTC",
|
||
dateStyle: "short",
|
||
});
|
||
|
||
expect(result).toContain("12/1");
|
||
expect(result).toContain("12/2");
|
||
});
|
||
});
|
||
|
||
describe("unsupported locales (dayjs fallback)", () => {
|
||
it("formats time range in Icelandic", () => {
|
||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||
const endDate = new Date("2025-12-01T11:30:00Z");
|
||
|
||
const result = formatDateTimeRange(startDate, endDate, {
|
||
locale: "is",
|
||
timeZone: "UTC",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
|
||
expect(result).toContain("10:30");
|
||
expect(result).toContain("11:30");
|
||
expect(result).toContain("–"); // En dash separator
|
||
});
|
||
|
||
it("formats date range in Lithuanian", () => {
|
||
const startDate = new Date("2025-12-01T10:30:00Z");
|
||
const endDate = new Date("2025-12-02T10:30:00Z");
|
||
|
||
const result = formatDateTimeRange(startDate, endDate, {
|
||
locale: "lt",
|
||
dateStyle: "medium",
|
||
});
|
||
|
||
expect(result).toContain("–");
|
||
expect(result).toContain("1");
|
||
expect(result).toContain("2");
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("formatWeekday", () => {
|
||
describe("supported locales", () => {
|
||
it("formats long weekday names in English", () => {
|
||
expect(formatWeekday("en-US", 0, "long")).toBe("Sunday");
|
||
expect(formatWeekday("en-US", 1, "long")).toBe("Monday");
|
||
expect(formatWeekday("en-US", 6, "long")).toBe("Saturday");
|
||
});
|
||
|
||
it("formats short weekday names in English", () => {
|
||
expect(formatWeekday("en-US", 0, "short")).toBe("Sun");
|
||
expect(formatWeekday("en-US", 1, "short")).toBe("Mon");
|
||
});
|
||
});
|
||
|
||
describe("unsupported locales (dayjs fallback)", () => {
|
||
it("formats long weekday names in Icelandic", () => {
|
||
const monday = formatWeekday("is", 1, "long");
|
||
expect(monday).toMatch(/mánudagur/i);
|
||
});
|
||
|
||
it("formats short weekday names in Icelandic", () => {
|
||
const monday = formatWeekday("is", 1, "short");
|
||
expect(monday).toMatch(/mán/i);
|
||
});
|
||
|
||
it("formats weekdays in Lithuanian", () => {
|
||
const monday = formatWeekday("lt", 1, "long");
|
||
expect(monday).toMatch(/pirmadienis/i); // Monday in Lithuanian
|
||
});
|
||
|
||
it("formats weekdays in Norwegian Bokmål", () => {
|
||
const monday = formatWeekday("nb", 1, "long");
|
||
expect(monday).toMatch(/mandag/i); // Monday in Norwegian
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("getWeekdayNames", () => {
|
||
it("returns 7 weekday names starting Sunday by default", () => {
|
||
const result = getWeekdayNames("en-US");
|
||
|
||
expect(result).toHaveLength(7);
|
||
expect(result[0]).toBe("Sunday");
|
||
expect(result[1]).toBe("Monday");
|
||
expect(result[6]).toBe("Saturday");
|
||
});
|
||
|
||
it("supports custom week start (Monday)", () => {
|
||
const result = getWeekdayNames("en-US", 1);
|
||
|
||
expect(result).toHaveLength(7);
|
||
expect(result[0]).toBe("Monday");
|
||
expect(result[6]).toBe("Sunday");
|
||
});
|
||
|
||
it("supports short format", () => {
|
||
const result = getWeekdayNames("en-US", 0, "short");
|
||
|
||
expect(result).toHaveLength(7);
|
||
expect(result[0]).toBe("Sun");
|
||
expect(result[1]).toBe("Mon");
|
||
});
|
||
|
||
it("returns Icelandic weekday names", () => {
|
||
const result = getWeekdayNames("is", 0, "long");
|
||
|
||
expect(result).toHaveLength(7);
|
||
// Validate at least one Icelandic weekday
|
||
expect(result.some((day) => day.match(/dagur$/i))).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("timezone handling", () => {
|
||
it("formats in UTC correctly for both Intl and dayjs paths", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
// Intl path (supported locale)
|
||
const intlResult = formatDateTime(date, {
|
||
locale: "en",
|
||
timeZone: "UTC",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
expect(intlResult).toContain("10:30");
|
||
|
||
// Dayjs path (unsupported locale)
|
||
const dayjsResult = formatDateTime(date, {
|
||
locale: "is",
|
||
timeZone: "UTC",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
expect(dayjsResult).toContain("10:30");
|
||
});
|
||
|
||
it("applies Tokyo timezone offset correctly", () => {
|
||
const date = new Date("2025-12-01T10:30:00Z");
|
||
|
||
// Intl path
|
||
const intlResult = formatDateTime(date, {
|
||
locale: "en",
|
||
timeZone: "Asia/Tokyo",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
expect(intlResult).toContain("19:30"); // UTC+9
|
||
|
||
// Test timezone respect in dayjs fallback
|
||
const dayjsResult = formatDateTime(date, {
|
||
locale: "is",
|
||
timeZone: "Asia/Tokyo",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
expect(dayjsResult).toContain("19:30"); // UTC+9
|
||
});
|
||
|
||
it("handles day boundary crossing with timezone offset", () => {
|
||
const date = new Date("2025-12-01T20:30:00Z");
|
||
|
||
const result = formatDateTime(date, {
|
||
locale: "is",
|
||
timeZone: "Asia/Tokyo",
|
||
dateStyle: "short",
|
||
timeStyle: "short",
|
||
hour12: false,
|
||
});
|
||
// 20:30 UTC + 9h = 05:30 next day
|
||
expect(result).toContain("05:30");
|
||
expect(result).toContain("2"); // December 2nd
|
||
});
|
||
});
|
||
});
|