* [#337] added timezone loading from backend * [#337] changed data structure to allow patching timezone while keeping other datetime params value * [#337] fixed gap, event create modale sync with settings and added checkbox * [#337] isBrowserDefaultTimezone is set to true when API return null timezone Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -2,11 +2,10 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import CalendarApp from "../../../src/components/Calendar/Calendar";
|
||||
import { updateSlotLabelVisibility } from "../../../src/components/Calendar/utils/calendarUtils";
|
||||
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
||||
import * as CalendarSlice from "../../../src/features/Calendars/CalendarSlice";
|
||||
import * as SettingsSlice from "../../../src/features/Settings/SettingsSlice";
|
||||
import * as calendarUtils from "../../../src/components/Calendar/utils/calendarUtils";
|
||||
import { CalendarEvent } from "../../../src/features/Events/EventsTypes";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import preview from "jest-preview";
|
||||
|
||||
describe("Calendar - Timezone Integration", () => {
|
||||
const mockCalendarRef = { current: null };
|
||||
@@ -56,7 +55,7 @@ describe("Calendar - Timezone Integration", () => {
|
||||
});
|
||||
|
||||
it("dispatches setTimeZone action when timezone is changed", async () => {
|
||||
const setTimeZoneSpy = jest.spyOn(CalendarSlice, "setTimeZone");
|
||||
const setTimeZoneSpy = jest.spyOn(SettingsSlice, "setTimeZone");
|
||||
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
@@ -226,8 +225,7 @@ describe("EventDisplayPreview - Timezone Display", () => {
|
||||
for (const { tz, expectedOffset } of timezones) {
|
||||
const state = {
|
||||
...baseState,
|
||||
calendars: {
|
||||
...baseState.calendars,
|
||||
settings: {
|
||||
timeZone: tz,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import SettingsPage from "../../../src/features/Settings/SettingsPage";
|
||||
import settingsReducer, {
|
||||
setIsBrowserDefaultTimeZone,
|
||||
setTimeZone,
|
||||
} from "../../../src/features/Settings/SettingsSlice";
|
||||
import userReducer, {
|
||||
getOpenPaasUserDataAsync,
|
||||
setTimezone as setUserTimeZone,
|
||||
} from "../../../src/features/User/userSlice";
|
||||
import { api } from "../../../src/utils/apiUtils";
|
||||
import { browserDefaultTimeZone } from "../../../src/utils/timezone";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
|
||||
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
|
||||
let apiGetSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
apiGetSpy = jest.spyOn(api, "get");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
apiGetSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should sync timezone to both user and settings state after fetching user data", async () => {
|
||||
const mockUserData = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: ["test@test.com"],
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
{ name: "language", value: "fr" },
|
||||
{ name: "datetime", value: { timeZone: "Europe/Paris" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Return a fake fetch-like response
|
||||
apiGetSpy.mockResolvedValue({
|
||||
json: async () => mockUserData,
|
||||
});
|
||||
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
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" } },
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: { language: "en", timeZone: "UTC", view: "calendar" },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync());
|
||||
|
||||
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe("Europe/Paris");
|
||||
expect(state.settings.timeZone).toBe("Europe/Paris");
|
||||
expect(localStorage.getItem("timeZone")).toBe("Europe/Paris");
|
||||
});
|
||||
|
||||
it("should keep browser timezone if API response has no timezone configuration", async () => {
|
||||
const mockUserData = {
|
||||
id: "667037022b752d0026472254",
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
preferredEmail: "[test@test.com](mailto:test@test.com)",
|
||||
configurations: {
|
||||
modules: [
|
||||
{ name: "core", configurations: [{ name: "language", value: "en" }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
apiGetSpy.mockResolvedValue({
|
||||
json: async () => mockUserData,
|
||||
});
|
||||
|
||||
const browserTimezone = browserDefaultTimeZone ?? "UTC";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: { user: userReducer, settings: settingsReducer },
|
||||
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: browserTimezone },
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: browserTimezone,
|
||||
view: "calendar",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await store.dispatch(getOpenPaasUserDataAsync());
|
||||
|
||||
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(state.settings.timeZone).toBe(browserTimezone);
|
||||
});
|
||||
});
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe("Timezone Logic - Backend to Frontend Flow", () => {
|
||||
let store: any;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
settings: settingsReducer,
|
||||
user: userReducer,
|
||||
},
|
||||
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" } },
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: { language: "en", timeZone: "UTC", view: "calendar" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("Backend Response Handling", () => {
|
||||
test("Backend returns NULL => Should use browser default timezone", async () => {
|
||||
const backendResponse = {
|
||||
firstname: "John",
|
||||
lastname: "Doe",
|
||||
id: "123",
|
||||
preferredEmail: "john@example.com",
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
value: {
|
||||
timeZone: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
|
||||
test("Backend returns SPECIFIC VALUE => Should use that value", async () => {
|
||||
const specificTimezone = "America/New_York";
|
||||
const backendResponse = {
|
||||
firstname: "Jane",
|
||||
lastname: "Smith",
|
||||
id: "456",
|
||||
preferredEmail: "jane@example.com",
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
value: {
|
||||
timeZone: specificTimezone,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
|
||||
expect(settingsState.timeZone).toBe(specificTimezone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(false);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(specificTimezone);
|
||||
expect(localStorage.getItem("timeZone")).toBe(specificTimezone);
|
||||
});
|
||||
|
||||
test("Backend returns NO datetime config => Should use browser default", async () => {
|
||||
const backendResponse = {
|
||||
firstname: "Bob",
|
||||
lastname: "Johnson",
|
||||
id: "789",
|
||||
preferredEmail: "bob@example.com",
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
// No datetime config
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
|
||||
);
|
||||
|
||||
const settingsState = store.getState().settings;
|
||||
const userState = store.getState().user;
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.isBrowserDefaultTimeZone).toBe(true);
|
||||
expect(userState.coreConfig.datetime.timeZone).toBe(null);
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
});
|
||||
|
||||
test("Settings state ALWAYS has a concrete value (never null)", () => {
|
||||
// Test 1: Browser default scenario
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true));
|
||||
let settingsState = store.getState().settings;
|
||||
|
||||
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(settingsState.timeZone).not.toBe(null);
|
||||
|
||||
// Test 2: Specific timezone scenario
|
||||
store.dispatch(setTimeZone("Europe/Paris"));
|
||||
settingsState = store.getState().settings;
|
||||
|
||||
expect(settingsState.timeZone).toBe("Europe/Paris");
|
||||
expect(settingsState.timeZone).not.toBe(null);
|
||||
});
|
||||
|
||||
describe("User Actions - Changing Timezone", () => {
|
||||
test("User enables browser default => Settings gets browser TZ, User gets null", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, {
|
||||
user: {
|
||||
userData: { sub: "test" },
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: {
|
||||
language: "en",
|
||||
datetime: { timeZone: "America/Los_Angeles" },
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: "America/Los_Angeles",
|
||||
isBrowserDefaultTimeZone: false,
|
||||
view: "settings",
|
||||
},
|
||||
});
|
||||
|
||||
const browserDefaultSwitch = screen.getAllByLabelText(
|
||||
"settings.timeZoneBrowserDefault"
|
||||
)[0];
|
||||
expect(browserDefaultSwitch).toBeInTheDocument();
|
||||
|
||||
// Enable browser default
|
||||
fireEvent.click(browserDefaultSwitch);
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(true);
|
||||
});
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
});
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
test("User selects specific timezone => Both states get the value", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, {
|
||||
user: {
|
||||
userData: { sub: "test" },
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
coreConfig: { language: "en", datetime: { timeZone: null } },
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
timeZone: browserDefaultTimeZone,
|
||||
isBrowserDefaultTimeZone: true,
|
||||
view: "settings",
|
||||
},
|
||||
});
|
||||
|
||||
const browserDefaultSwitch = screen.getAllByLabelText(
|
||||
"settings.timeZoneBrowserDefault"
|
||||
)[0];
|
||||
|
||||
// Disable browser default so manual selector appears
|
||||
fireEvent.click(browserDefaultSwitch);
|
||||
|
||||
// Now timezone combobox is visible
|
||||
const timezoneInput = screen.getAllByRole("combobox")[1];
|
||||
|
||||
// Type to filter options
|
||||
fireEvent.change(timezoneInput, {
|
||||
target: { value: "Australia/Sydney" },
|
||||
});
|
||||
|
||||
// Select from autocomplete dropdown
|
||||
const option = await screen.findByText(/Australia\/Sydney/i);
|
||||
fireEvent.click(option);
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.isBrowserDefaultTimeZone).toBe(false);
|
||||
});
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.timeZone).toBe("Australia/Sydney");
|
||||
});
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.user.coreConfig.datetime.timeZone).toBe(
|
||||
"Australia/Sydney"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalStorage Persistence", () => {
|
||||
test("LocalStorage always stores concrete values (browser TZ or specific TZ)", () => {
|
||||
// Scenario 1: Browser default
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone));
|
||||
expect(localStorage.getItem("timeZone")).toBe(browserDefaultTimeZone);
|
||||
expect(localStorage.getItem("timeZone")).not.toBe("null");
|
||||
|
||||
// Scenario 2: Specific timezone
|
||||
store.dispatch(setTimeZone("Africa/Cairo"));
|
||||
expect(localStorage.getItem("timeZone")).toBe("Africa/Cairo");
|
||||
});
|
||||
});
|
||||
|
||||
test("Full user journey: Backend null -> User changes -> Backend saves", async () => {
|
||||
// Step 1: Backend returns null
|
||||
const backendResponseNull = {
|
||||
firstname: "Test",
|
||||
lastname: "User",
|
||||
id: "999",
|
||||
preferredEmail: "test@example.com",
|
||||
configurations: {
|
||||
modules: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
{
|
||||
name: "datetime",
|
||||
value: { timeZone: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await store.dispatch(
|
||||
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, "", undefined)
|
||||
);
|
||||
|
||||
// Verify initial state
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null);
|
||||
|
||||
// Step 2: User changes to specific timezone
|
||||
const userSelectedTZ = "Europe/London";
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(false));
|
||||
store.dispatch(setTimeZone(userSelectedTZ));
|
||||
store.dispatch(setUserTimeZone(userSelectedTZ));
|
||||
|
||||
expect(store.getState().settings.timeZone).toBe(userSelectedTZ);
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(
|
||||
userSelectedTZ
|
||||
);
|
||||
|
||||
// Step 3: User switches back to browser default
|
||||
store.dispatch(setIsBrowserDefaultTimeZone(true));
|
||||
store.dispatch(setUserTimeZone(null));
|
||||
store.dispatch(setTimeZone(browserDefaultTimeZone));
|
||||
|
||||
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone);
|
||||
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ describe("SettingsPage", () => {
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
language: "en",
|
||||
coreConfig: { language: "en" },
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
@@ -47,6 +47,7 @@ describe("SettingsPage", () => {
|
||||
const { container } = renderWithProviders(<SettingsPage />, 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);
|
||||
@@ -56,6 +57,7 @@ describe("SettingsPage", () => {
|
||||
it("highlights active navigation item", () => {
|
||||
const { container } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
|
||||
const settingsNavItem = container.querySelector(
|
||||
".settings-nav-item.active"
|
||||
);
|
||||
@@ -108,7 +110,7 @@ describe("SettingsPage", () => {
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
language: "fr",
|
||||
coreConfig: { language: "fr" },
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
@@ -125,6 +127,7 @@ describe("SettingsPage", () => {
|
||||
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;
|
||||
@@ -140,6 +143,7 @@ describe("SettingsPage", () => {
|
||||
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;
|
||||
@@ -155,7 +159,10 @@ describe("SettingsPage", () => {
|
||||
// Language should be updated immediately (optimistic update)
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.user?.language).toBe("fr");
|
||||
expect(state.user?.coreConfig.language).toBe("fr");
|
||||
});
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.language).toBe("fr");
|
||||
});
|
||||
|
||||
@@ -185,6 +192,7 @@ describe("SettingsPage", () => {
|
||||
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;
|
||||
@@ -211,6 +219,7 @@ describe("SettingsPage", () => {
|
||||
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;
|
||||
@@ -224,10 +233,13 @@ describe("SettingsPage", () => {
|
||||
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.user?.language).toBe("en");
|
||||
expect(state.settings.language).toBe("en");
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
@@ -246,4 +258,135 @@ describe("SettingsPage", () => {
|
||||
screen.getByText("settings.notifications.empty")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Timezone Settings", () => {
|
||||
it("displays timezone selector in Settings tab", () => {
|
||||
renderWithProviders(<SettingsPage />, 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",
|
||||
view: "settings",
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<SettingsPage />, stateWithUserTimeZone);
|
||||
|
||||
expect(screen.getByDisplayValue(/America\/New York/i)).toBeDefined();
|
||||
});
|
||||
|
||||
it("updates timezone immediately (optimistic update)", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, 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");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles timezone change with different timezone values", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, 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");
|
||||
});
|
||||
|
||||
// Test with America/Los_Angeles
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
view: "settings",
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<SettingsPage />, stateWithoutTimeZone);
|
||||
|
||||
const timezoneInput = screen.getByDisplayValue("(UTC) UTC");
|
||||
expect(timezoneInput).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,15 +23,37 @@ jest.mock("../../../src/features/User/oidcAuth", () => ({
|
||||
Callback: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/features/User/userSlice", () => ({
|
||||
setUserData: jest.fn((data) => ({ type: "SET_USER", payload: data })),
|
||||
setTokens: jest.fn((tokens) => ({ type: "SET_TOKENS", payload: tokens })),
|
||||
getOpenPaasUserDataAsync: jest.fn(() => ({ type: "GET_USER_ID" })),
|
||||
}));
|
||||
jest.mock("../../../src/features/User/userSlice", () => {
|
||||
const mockGetUser = Object.assign(
|
||||
jest.fn(() => ({ type: "GET_USER_ID" })),
|
||||
{
|
||||
pending: { type: "GET_USER_ID/pending" },
|
||||
fulfilled: { type: "GET_USER_ID/fulfilled" },
|
||||
rejected: { type: "GET_USER_ID/rejected" },
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock("../../../src/features/Calendars/CalendarSlice", () => ({
|
||||
getCalendarsListAsync: jest.fn(() => ({ type: "GET_CALENDARS" })),
|
||||
}));
|
||||
return {
|
||||
setUserData: jest.fn((data) => ({ type: "SET_USER", payload: data })),
|
||||
setTokens: jest.fn((tokens) => ({ type: "SET_TOKENS", payload: tokens })),
|
||||
getOpenPaasUserDataAsync: mockGetUser,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../../src/features/Calendars/CalendarSlice", () => {
|
||||
const mockGetCalendars = Object.assign(
|
||||
jest.fn(() => ({ type: "GET_CALENDARS" })),
|
||||
{
|
||||
pending: { type: "GET_CALENDARS/pending" },
|
||||
fulfilled: { type: "GET_CALENDARS/fulfilled" },
|
||||
rejected: { type: "GET_CALENDARS/rejected" },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
getCalendarsListAsync: mockGetCalendars,
|
||||
};
|
||||
});
|
||||
|
||||
describe("CallbackResume", () => {
|
||||
const dispatch = jest.fn();
|
||||
@@ -61,12 +83,26 @@ describe("CallbackResume", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(oidcAuth.Callback).toHaveBeenCalledWith("verifier123", "state456");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setUserData(mockUserInfo));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(setTokens(mockTokenSet));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(getOpenPaasUserDataAsync());
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(getCalendarsListAsync());
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(dispatch).toHaveBeenCalledWith(push("/"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem("redirectState")).toBe(null);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem("tokenSet")).toEqual(
|
||||
JSON.stringify(mockTokenSet)
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("updateUserConfigurations", () => {
|
||||
name: "core",
|
||||
configurations: [
|
||||
{ name: "language", value: "fr" },
|
||||
{ name: "timezone", value: "Europe/Paris" },
|
||||
{ name: "datetime", value: { timeZone: "Europe/Paris" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user