Fix: Preserve timezone used in create and update modal (#301)
- Fix timezone field display in event modals to show original timezone from server - Fix formatDateToICal to use moment.utc(date).tz(timezone) for correct UTC instant preservation - Fix parseCalendarEvent to convert event.start/end to ISO UTC strings after parsing - Fix eventToFullCalendarFormat to ensure event times are ISO UTC before passing to FullCalendar - Fix getEvent to normalize event.start/end to ISO UTC strings - Ensure event.timezone is always set (defaults to Etc/UTC if not detected) - Update eventUtils.test.ts to match new timezone conversion logic - Fix event time shift issue on calendar grid display This ensures: - Events display in correct time slots on calendar grid - Event times are correctly converted between timezones - Original timezone from server is preserved and displayed - All event times are stored as ISO UTC strings internally * refactor: eliminate datetime format detection code duplication - Add constants for datetime format strings and magic number (DATETIME_WITH_SECONDS_LENGTH, DATETIME_FORMAT_WITH_SECONDS, DATETIME_FORMAT_WITHOUT_SECONDS) - Create shared detectDateTimeFormat() function to replace duplicated format detection logic - Refactor convertFormDateTimeToISO, convertEventDateTimeToISO, and convertDateTimeStringToISO to use shared helper - Add console.warn logging when invalid datetime is encountered (addresses silent fallback issue) - Add comprehensive test coverage for dateTimeHelpers with full test suite
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
detectDateTimeFormat,
|
||||
convertFormDateTimeToISO,
|
||||
DATETIME_WITH_SECONDS_LENGTH,
|
||||
DATETIME_FORMAT_WITH_SECONDS,
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS,
|
||||
} from "../../../../src/components/Event/utils/dateTimeHelpers";
|
||||
|
||||
describe("dateTimeHelpers", () => {
|
||||
describe("Constants", () => {
|
||||
it("should have correct constant values", () => {
|
||||
expect(DATETIME_WITH_SECONDS_LENGTH).toBe(19);
|
||||
expect(DATETIME_FORMAT_WITH_SECONDS).toBe("YYYY-MM-DDTHH:mm:ss");
|
||||
expect(DATETIME_FORMAT_WITHOUT_SECONDS).toBe("YYYY-MM-DDTHH:mm");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectDateTimeFormat", () => {
|
||||
it("should return format with seconds for length >= 19", () => {
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30:45")).toBe(
|
||||
DATETIME_FORMAT_WITH_SECONDS
|
||||
);
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30:45")).toBe(
|
||||
"YYYY-MM-DDTHH:mm:ss"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return format without seconds for length < 19", () => {
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30")).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
expect(detectDateTimeFormat("2024-01-15T10:30")).toBe("YYYY-MM-DDTHH:mm");
|
||||
});
|
||||
|
||||
it("should return format with seconds for length exactly 19", () => {
|
||||
const datetime = "2024-01-15T10:30:45";
|
||||
expect(datetime.length).toBe(19);
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS);
|
||||
});
|
||||
|
||||
it("should return format without seconds for length 16", () => {
|
||||
const datetime = "2024-01-15T10:30";
|
||||
expect(datetime.length).toBe(16);
|
||||
expect(detectDateTimeFormat(datetime)).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
});
|
||||
|
||||
it("should return format with seconds for length > 19", () => {
|
||||
const datetime = "2024-01-15T10:30:45.123";
|
||||
expect(datetime.length).toBeGreaterThan(19);
|
||||
expect(detectDateTimeFormat(datetime)).toBe(DATETIME_FORMAT_WITH_SECONDS);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(detectDateTimeFormat("")).toBe(DATETIME_FORMAT_WITHOUT_SECONDS);
|
||||
});
|
||||
|
||||
it("should handle very short strings", () => {
|
||||
expect(detectDateTimeFormat("2024")).toBe(
|
||||
DATETIME_FORMAT_WITHOUT_SECONDS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertFormDateTimeToISO", () => {
|
||||
const originalConsoleWarn = console.warn;
|
||||
let consoleWarnSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should convert valid datetime without seconds to ISO string", () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should convert valid datetime with seconds to ISO string", () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30:45",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use Etc/UTC as default timezone when timezone is empty", () => {
|
||||
const result = convertFormDateTimeToISO("2024-01-15T10:30", "");
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it("should return empty string for empty datetime input", () => {
|
||||
const result = convertFormDateTimeToISO("", "America/New_York");
|
||||
expect(result).toBe("");
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return empty string and log warning for invalid datetime", () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"invalid-date",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBe("");
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[convertFormDateTimeToISO] Invalid datetime:")
|
||||
);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"invalid-date"')
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty string and log warning for invalid format", () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-13-45T25:99:99",
|
||||
"America/New_York"
|
||||
);
|
||||
expect(result).toBe("");
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle different timezones correctly", () => {
|
||||
const result1 = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"America/New_York"
|
||||
);
|
||||
const result2 = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
"Europe/London"
|
||||
);
|
||||
expect(result1).toBeTruthy();
|
||||
expect(result2).toBeTruthy();
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it("should handle edge case with null/undefined timezone", () => {
|
||||
const result = convertFormDateTimeToISO(
|
||||
"2024-01-15T10:30",
|
||||
// @ts-ignore - testing edge case
|
||||
null
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should convert correctly for UTC timezone", () => {
|
||||
const result = convertFormDateTimeToISO("2024-01-15T10:30", "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toContain("T10:30:00");
|
||||
});
|
||||
|
||||
it("should handle datetime at boundary (exactly 19 characters)", () => {
|
||||
const datetime = "2024-01-15T10:30:45";
|
||||
expect(datetime.length).toBe(19);
|
||||
const result = convertFormDateTimeToISO(datetime, "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle datetime at boundary (exactly 16 characters)", () => {
|
||||
const datetime = "2024-01-15T10:30";
|
||||
expect(datetime.length).toBe(16);
|
||||
const result = convertFormDateTimeToISO(datetime, "Etc/UTC");
|
||||
expect(result).toBeTruthy();
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user