From 2ba92ed194600e4e241fb56c88337985cb1c83d7 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:27:54 +0100 Subject: [PATCH] [#632] fix to adapt headers to proper selected timezone (#674) Co-authored-by: Camille Moussu --- .../components/Calendar.timezone.test.tsx | 106 ++++++++++++++++++ src/components/Calendar/Calendar.tsx | 19 +++- .../Timezone/TimezoneAutocomplete.tsx | 1 + 3 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 __test__/components/Calendar.timezone.test.tsx diff --git a/__test__/components/Calendar.timezone.test.tsx b/__test__/components/Calendar.timezone.test.tsx new file mode 100644 index 0000000..ad77f50 --- /dev/null +++ b/__test__/components/Calendar.timezone.test.tsx @@ -0,0 +1,106 @@ +import CalendarLayout from "@/components/Calendar/CalendarLayout"; +import { act, waitFor } from "@testing-library/react"; +import { renderWithProviders } from "../utils/Renderwithproviders"; + +const PIVOT_UTC = new Date("2026-03-16T17:00:00Z"); + +const makeState = (timezone: string) => ({ + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "mockSid", + openpaasId: "user1", + }, + tokens: { accessToken: "token" }, + }, + settings: { view: "calendar", timeZone: timezone }, + calendars: { + list: { + "user1/cal1": { + name: "Calendar personal", + id: "user1/cal1", + color: { light: "#FF0000", dark: "#000" }, + owner: { emails: ["test@test.com"] }, + events: {}, + }, + }, + pending: false, + }, +}); + +describe("Calendar – dayHeaderContent respects selected timezone", () => { + afterEach(() => { + jest.useRealTimers(); + }); + + async function renderAtPivotTime(timezone: string): Promise { + // Set fake timers the same way the working test does — single chained call + jest.useFakeTimers().setSystemTime(PIVOT_UTC); + + await act(async () => { + renderWithProviders(, makeState(timezone)); + }); + + // Advance past the debounce (300ms) like the working navigation test does + await act(async () => { + jest.advanceTimersByTime(300); + }); + + const calendarRef = window.__calendarRef; + await waitFor(() => expect(calendarRef?.current).not.toBeNull()); + + await act(async () => { + calendarRef.current?.changeView("timeGridWeek"); + }); + + await waitFor(() => { + expect(document.querySelectorAll(".fc-col-header-cell").length).toBe(7); + }); + + return Array.from(document.querySelectorAll(".fc-col-header-cell")).map( + (cell) => cell.textContent ?? "" + ); + } + + it("UTC: Monday March 16 column exists", async () => { + const cells = await renderAtPivotTime("UTC"); + const monday = cells.find((t) => t.includes("16")); + expect(monday).toBeDefined(); + expect(monday).toMatch(/MON/i); + }); + + it("Europe/Paris: Monday March 16 column exists (UTC+1, no day shift)", async () => { + const cells = await renderAtPivotTime("Europe/Paris"); + const monday = cells.find((t) => t.includes("16")); + expect(monday).toBeDefined(); + expect(monday).toMatch(/MON/i); + }); + + it("Asia/Jakarta: Tuesday March 17 column exists (UTC+7, day shifts forward)", async () => { + const cells = await renderAtPivotTime("Asia/Jakarta"); + const tuesday = cells.find((t) => t.includes("17")); + expect(tuesday).toBeDefined(); + expect(tuesday).toMatch(/TUE/i); + }); + + it("Asia/Jakarta: no column incorrectly labeled MON 17 – regression", async () => { + const cells = await renderAtPivotTime("Asia/Jakarta"); + const wrongCell = cells.find((t) => t.includes("17") && /MON/i.test(t)); + expect(wrongCell).toBeUndefined(); + }); + + it("Asia/Tokyo: Tuesday March 17 column exists (UTC+9)", async () => { + const cells = await renderAtPivotTime("Asia/Tokyo"); + const tuesday = cells.find((t) => t.includes("17")); + expect(tuesday).toBeDefined(); + expect(tuesday).toMatch(/TUE/i); + }); + + it("America/New_York: Monday March 16 column exists (UTC-4, no day shift)", async () => { + const cells = await renderAtPivotTime("America/New_York"); + const monday = cells.find((t) => t.includes("16")); + expect(monday).toBeDefined(); + expect(monday).toMatch(/MON/i); + }); +}); diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 33ff844..cd72ea7 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -22,6 +22,7 @@ import FullCalendar from "@fullcalendar/react"; import timeGridPlugin from "@fullcalendar/timegrid"; import { Box, Button, radius } from "@linagora/twake-mui"; import AddIcon from "@mui/icons-material/Add"; +import moment from "moment-timezone"; import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; import { useCalendarDataLoader } from "../../features/Calendars/useCalendarLoader"; @@ -467,6 +468,7 @@ export default function CalendarApp({ dayCellContent={(arg) => { const month = arg.date.toLocaleDateString(t("locale"), { month: "short", + timeZone: timezone, }); if (arg.view.type === "dayGridMonth") { return ( @@ -523,18 +525,23 @@ export default function CalendarApp({ }, 100); }} dayHeaderContent={(arg) => { - const date = arg.date.getDate(); - const weekDay = arg.date - .toLocaleDateString(t("locale"), { weekday: "short" }) + const m = moment.tz(arg.date, timezone); + + const date = m.date(); + const weekDay = m + .toDate() + .toLocaleDateString(t("locale"), { + weekday: "short", + timeZone: timezone, + }) .toUpperCase(); + return (
{weekDay} {arg.view.type !== "dayGridMonth" && ( {date} diff --git a/src/components/Timezone/TimezoneAutocomplete.tsx b/src/components/Timezone/TimezoneAutocomplete.tsx index fee4852..aa17098 100644 --- a/src/components/Timezone/TimezoneAutocomplete.tsx +++ b/src/components/Timezone/TimezoneAutocomplete.tsx @@ -72,6 +72,7 @@ export function TimezoneAutocomplete({ e.target.select()} variant="outlined" autoComplete="off" inputRef={inputRef}