import SettingsPage from "@/features/Settings/SettingsPage"; import { api } from "@/utils/apiUtils"; import "@testing-library/jest-dom"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import { renderWithProviders } from "../../utils/Renderwithproviders"; jest.mock("@/utils/apiUtils"); describe("SettingsPage", () => { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); }); const preloadedState = { user: { userData: { sub: "test", email: "test@test.com", family_name: "Doe", name: "John", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, organiserData: null, tokens: null, coreConfig: { language: "en", datetime: { timeZone: "UTC" }, }, alarmEmailsEnabled: false, loading: false, error: null, }, settings: { language: "en", timeZone: "UTC", isBrowserDefaultTimeZone: false, view: "settings", }, }; it("renders settings page with sidebar and content layout", () => { renderWithProviders(, preloadedState); expect(screen.getByRole("main")).toHaveClass( "main-layout", "settings-layout" ); }); it("displays sidebar navigation with Settings and Notifications items", () => { const { container } = renderWithProviders(, preloadedState); // Check sidebar navigation items // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const sidebar = container.querySelector(".settings-sidebar"); expect(sidebar).toBeInTheDocument(); expect(screen.getAllByText(/settings.title/i).length).toBeGreaterThan(0); expect(screen.getByText(/settings.notifications/i)).toBeInTheDocument(); }); it("highlights active navigation item", () => { const { container } = renderWithProviders(, preloadedState); // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const settingsNavItem = container.querySelector( ".settings-nav-item.active" ); expect(settingsNavItem).toBeInTheDocument(); }); it("navigates back to calendar view when clicking back button", async () => { const { store } = renderWithProviders(, preloadedState); const backButton = screen.getByLabelText("settings.back"); fireEvent.click(backButton); await waitFor(() => { const state = store.getState(); expect(state.settings.view).toBe("calendar"); }); }); it("switches between Settings and Notifications tabs", () => { renderWithProviders(, preloadedState); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); expect( screen.getByText(/settings.notifications.deliveryMethod/i) ).toBeInTheDocument(); }); it("displays language select in Settings tab", () => { renderWithProviders(, preloadedState); expect( screen.getByLabelText("settings.languageSelector") ).toBeInTheDocument(); }); it("displays all available language options and uses language from user state", async () => { const stateWithUserLanguage = { user: { userData: { sub: "test", email: "test@test.com", family_name: "Doe", name: "John", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, organiserData: null, tokens: null, coreConfig: { language: "fr", datetime: { timeZone: "UTC" } }, loading: false, error: null, }, settings: { language: "en", timeZone: "UTC", isBrowserDefaultTimeZone: false, view: "settings", }, }; renderWithProviders(, stateWithUserLanguage); const languageSelect = screen.getByLabelText("settings.languageSelector"); // Verify Select exists expect(languageSelect).toBeInTheDocument(); // Verify that the underlying native input reflects the user language ("fr") // eslint-disable-next-line testing-library/no-node-access const nativeInput = languageSelect.querySelector( 'input[aria-hidden="true"]' ) as HTMLInputElement | null; expect(nativeInput).not.toBeNull(); expect(nativeInput?.value).toBe("fr"); }); it("updates language immediately (optimistic update) and calls API in background", async () => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); const { store } = renderWithProviders(, preloadedState); const languageSelect = screen.getByLabelText("settings.languageSelector"); // MUI Select uses a native input element - find and change it // eslint-disable-next-line testing-library/no-node-access const nativeInput = languageSelect.querySelector( 'input[aria-hidden="true"]' ) as HTMLInputElement; expect(nativeInput).toBeInTheDocument(); // Simulate change event on the native input Object.defineProperty(nativeInput, "value", { writable: true, value: "fr", }); fireEvent.change(nativeInput, { target: { value: "fr" } }); // Language should be updated immediately (optimistic update) await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.language).toBe("fr"); }); await waitFor(() => { const state = store.getState(); expect(state.settings.language).toBe("fr"); }); // API should be called in background await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "language", value: "fr" }), ]), }), ]), }) ); }); }); it("saves language change to localStorage immediately (optimistic update)", async () => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); renderWithProviders(, preloadedState); const languageSelect = screen.getByLabelText("settings.languageSelector"); // MUI Select uses a native input element - find and change it // eslint-disable-next-line testing-library/no-node-access const nativeInput = languageSelect.querySelector( 'input[aria-hidden="true"]' ) as HTMLInputElement; expect(nativeInput).toBeInTheDocument(); // Simulate change event on the native input Object.defineProperty(nativeInput, "value", { writable: true, value: "fr", }); fireEvent.change(nativeInput, { target: { value: "fr" } }); // localStorage should be updated immediately (optimistic update) await waitFor(() => { expect(localStorage.getItem("lang")).toBe("fr"); }); }); it("rolls back language change if API call fails", async () => { (api.patch as jest.Mock).mockRejectedValue(new Error("API Error")); const { store } = renderWithProviders(, preloadedState); const languageSelect = screen.getByLabelText("settings.languageSelector"); // MUI Select uses a native input element - find and change it // eslint-disable-next-line testing-library/no-node-access const nativeInput = languageSelect.querySelector( 'input[aria-hidden="true"]' ) as HTMLInputElement; expect(nativeInput).toBeInTheDocument(); // Simulate change event on the native input Object.defineProperty(nativeInput, "value", { writable: true, value: "fr", }); fireEvent.change(nativeInput, { target: { value: "fr" } }); // Wait for rollback - language should be rolled back to "en" after error await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.language).toBe("en"); }); await waitFor( () => { const state = store.getState(); expect(state.settings.language).toBe("en"); }, { timeout: 3000 } ); }); it("shows Email toggle in Notifications tab", () => { renderWithProviders(, preloadedState); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); expect( screen.getByText(/settings.notifications.deliveryMethod/i) ).toBeInTheDocument(); expect( screen.getByRole("switch", { name: /settings.notifications.email/i }) ).toBeInTheDocument(); }); describe("Timezone Settings", () => { beforeEach(() => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); }); it("displays timezone selector in Settings tab", () => { renderWithProviders(, preloadedState); expect(screen.getAllByRole("combobox")).toHaveLength(2); }); it("displays timezone from user state", () => { const stateWithUserTimeZone = { user: { userData: { sub: "test", email: "test@test.com", family_name: "Doe", name: "John", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, organiserData: null, tokens: null, coreConfig: { language: "en", datetime: { timeZone: "America/New_York" }, }, loading: false, error: null, }, settings: { language: "en", timeZone: "UTC", isBrowserDefaultTimeZone: false, view: "settings", }, }; renderWithProviders(, stateWithUserTimeZone); expect(screen.getByDisplayValue(/America\/New York/i)).toBeDefined(); }); it("updates timezone immediately (optimistic update) and calls API", async () => { const { store } = renderWithProviders(, preloadedState); const timezoneInput = screen.getAllByRole("combobox")[1]; // Clear the input and type new timezone fireEvent.change(timezoneInput, { target: { value: "Europe/Paris" } }); // Find and click the timezone option from the dropdown const option = await screen.findByText(/Europe\/Paris/i); fireEvent.click(option); // Timezone should be updated immediately (optimistic update) await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBe("Europe/Paris"); }); await waitFor(() => { const state = store.getState(); expect(state.settings.timeZone).toBe("Europe/Paris"); }); await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "datetime", value: expect.objectContaining({ timeZone: "Europe/Paris", }), }), ]), }), ]), }) ); }); }); it("handles timezone change with different timezone values", async () => { const { store } = renderWithProviders(, preloadedState); const timezoneInput = screen.getAllByRole("combobox")[1]; // Test with Asia/Tokyo fireEvent.change(timezoneInput, { target: { value: "Asia/Tokyo" } }); const tokyoOption = await screen.findByText(/Asia\/Tokyo/i); fireEvent.click(tokyoOption); await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBe("Asia/Tokyo"); }); await waitFor(() => { const state = store.getState(); expect(state.settings.timeZone).toBe("Asia/Tokyo"); }); await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "datetime", value: expect.objectContaining({ timeZone: "Asia/Tokyo", }), }), ]), }), ]), }) ); }); jest.clearAllMocks(); (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); fireEvent.change(timezoneInput, { target: { value: "America/Los_Angeles" }, }); const laOption = await screen.findByText(/America\/Los Angeles/i); fireEvent.click(laOption); await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBe( "America/Los_Angeles" ); }); await waitFor(() => { const state = store.getState(); expect(state.settings.timeZone).toBe("America/Los_Angeles"); }); await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "datetime", value: expect.objectContaining({ timeZone: "America/Los_Angeles", }), }), ]), }), ]), }) ); }); }); it("enables browser default timezone and calls API with null", async () => { const { store } = renderWithProviders(, preloadedState); const browserDefaultSwitch = screen.getByRole("switch", { name: /settings.timeZoneBrowserDefault/i, }); expect(browserDefaultSwitch).not.toBeChecked(); fireEvent.click(browserDefaultSwitch); await waitFor(() => { expect(browserDefaultSwitch).toBeChecked(); }); await waitFor(() => { const state = store.getState(); expect(state.settings.isBrowserDefaultTimeZone).toBe(true); }); await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBeNull(); }); await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "datetime", value: expect.objectContaining({ timeZone: null, }), }), ]), }), ]), }) ); }); }); it("disables browser default timezone and shows timezone selector", async () => { const stateWithBrowserDefault = { ...preloadedState, user: { ...preloadedState.user, coreConfig: { language: "en", datetime: { timeZone: null }, }, }, settings: { ...preloadedState.settings, isBrowserDefaultTimeZone: true, }, }; renderWithProviders(, stateWithBrowserDefault); const browserDefaultSwitch = screen.getByRole("switch", { name: /settings.timeZoneBrowserDefault/i, }); expect(browserDefaultSwitch).toBeChecked(); expect(screen.getAllByRole("combobox")).toHaveLength(1); fireEvent.click(browserDefaultSwitch); await waitFor(() => { expect(browserDefaultSwitch).not.toBeChecked(); }); await waitFor(() => { expect(screen.getAllByRole("combobox")).toHaveLength(2); }); }); it("rolls back timezone change if API call fails", async () => { (api.patch as jest.Mock).mockRejectedValue(new Error("API Error")); const { store } = renderWithProviders(, preloadedState); const timezoneInput = screen.getAllByRole("combobox")[1]; fireEvent.change(timezoneInput, { target: { value: "Europe/Paris" } }); const option = await screen.findByText(/Europe\/Paris/i); fireEvent.click(option); await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBe("UTC"); }); await waitFor( () => { const state = store.getState(); expect(state.settings.timeZone).toBe("UTC"); }, { timeout: 3000 } ); await waitFor(() => { expect( screen.getByText("settings.timeZoneUpdateError") ).toBeInTheDocument(); }); }); it("rolls back browser default change if API call fails", async () => { (api.patch as jest.Mock).mockRejectedValue(new Error("API Error")); const { store } = renderWithProviders(, preloadedState); const browserDefaultSwitch = screen.getByRole("switch", { name: /settings.timeZoneBrowserDefault/i, }); fireEvent.click(browserDefaultSwitch); await waitFor( () => { const state = store.getState(); expect(state.settings.isBrowserDefaultTimeZone).toBe(false); }, { timeout: 3000 } ); await waitFor(() => { const state = store.getState(); expect(state.user?.coreConfig.datetime.timeZone).toBe("UTC"); }); await waitFor(() => { expect( screen.getByText("settings.timeZoneUpdateError") ).toBeInTheDocument(); }); }); it("uses UTC as default timezone when no timezone is set", () => { const stateWithoutTimeZone = { user: { userData: { sub: "test", email: "test@test.com", family_name: "Doe", name: "John", sid: "mockSid", openpaasId: "667037022b752d0026472254", }, organiserData: null, tokens: null, coreConfig: { language: "en", datetime: { timeZone: undefined } }, loading: false, error: null, }, settings: { language: "en", timeZone: undefined, isBrowserDefaultTimeZone: false, view: "settings", }, }; renderWithProviders(, stateWithoutTimeZone); const timezoneInput = screen.getByDisplayValue("(UTC) UTC"); expect(timezoneInput).toBeDefined(); }); it("preserves other datetime properties when updating timezone", async () => { const stateWithDatetimeConfig = { ...preloadedState, user: { ...preloadedState.user, coreConfig: { language: "en", datetime: { timeZone: "UTC", format: "24h", firstDayOfWeek: 1, }, }, }, }; renderWithProviders(, stateWithDatetimeConfig); const timezoneInput = screen.getAllByRole("combobox")[1]; fireEvent.change(timezoneInput, { target: { value: "Europe/Paris" } }); const option = await screen.findByText(/Europe\/Paris/i); fireEvent.click(option); await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "core", configurations: expect.arrayContaining([ expect.objectContaining({ name: "datetime", value: { timeZone: "Europe/Paris", format: "24h", firstDayOfWeek: 1, }, }), ]), }), ]), }) ); }); }); describe("Alarm Emails Settings", () => { beforeEach(() => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); }); it("displays alarmEmails toggle with correct initial state", () => { const stateWithAlarmEmailsEnabled = { ...preloadedState, user: { ...preloadedState.user, alarmEmailsEnabled: true, }, }; renderWithProviders(, stateWithAlarmEmailsEnabled); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); const toggle = screen.getByRole("switch", { name: /settings.notifications.email/i, }) as HTMLInputElement; expect(toggle).toBeInTheDocument(); expect(toggle.checked).toBe(true); }); it("displays alarmEmails toggle as true when alarmEmailsEnabled is null", () => { const stateWithNullAlarmEmails = { ...preloadedState, user: { ...preloadedState.user, alarmEmailsEnabled: null, }, }; renderWithProviders(, stateWithNullAlarmEmails); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); const toggle = screen.getByRole("switch", { name: /settings.notifications.email/i, }) as HTMLInputElement; expect(toggle).toBeInTheDocument(); expect(toggle.checked).toBe(true); }); it("updates alarmEmails immediately (optimistic update) and calls API in background", async () => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); const { store } = renderWithProviders(, preloadedState); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); const toggle = screen.getByRole("switch", { name: /settings.notifications.email/i, }) as HTMLInputElement; expect(toggle.checked).toBe(false); fireEvent.click(toggle); // AlarmEmails should be updated immediately (optimistic update) await waitFor(() => { const state = store.getState(); expect(state.user?.alarmEmailsEnabled).toBe(true); }); // API should be called in background await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "calendar", configurations: expect.arrayContaining([ expect.objectContaining({ name: "alarmEmails", value: true, }), ]), }), ]), }) ); }); }); it("rolls back alarmEmails change if API call fails", async () => { (api.patch as jest.Mock).mockRejectedValue(new Error("API Error")); const { store } = renderWithProviders(, preloadedState); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); const toggle = screen.getByRole("switch", { name: /settings.notifications.email/i, }) as HTMLInputElement; expect(toggle.checked).toBe(false); fireEvent.click(toggle); // Wait for rollback - alarmEmails should be rolled back to false after error await waitFor( () => { const state = store.getState(); expect(state.user?.alarmEmailsEnabled).toBe(false); }, { timeout: 3000 } ); }); it("sends false value when toggle is turned off", async () => { (api.patch as jest.Mock).mockResolvedValue({ status: 204 }); const stateWithAlarmEmailsEnabled = { ...preloadedState, user: { ...preloadedState.user, alarmEmailsEnabled: true, }, }; const { store } = renderWithProviders( , stateWithAlarmEmailsEnabled ); const notificationsTab = screen.getByRole("tab", { name: /settings.notifications/i, }); fireEvent.click(notificationsTab); const toggle = screen.getByRole("switch", { name: /settings.notifications.email/i, }) as HTMLInputElement; expect(toggle.checked).toBe(true); fireEvent.click(toggle); // AlarmEmails should be updated immediately (optimistic update) await waitFor(() => { const state = store.getState(); expect(state.user?.alarmEmailsEnabled).toBe(false); }); // API should be called with false value await waitFor(() => { expect(api.patch).toHaveBeenCalledWith( "api/configurations?scope=user", expect.objectContaining({ json: expect.arrayContaining([ expect.objectContaining({ name: "calendar", configurations: expect.arrayContaining([ expect.objectContaining({ name: "alarmEmails", value: false, }), ]), }), ]), }) ); }); }); }); }); });