[#339] added API calls to timezone selectors (#396)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-12-07 07:43:00 +01:00
committed by GitHub
parent 7567bfbd7b
commit 1056220719
4 changed files with 310 additions and 32 deletions
@@ -13,6 +13,8 @@ import { api } from "../../../src/utils/apiUtils";
import { browserDefaultTimeZone } from "../../../src/utils/timezone";
import { renderWithProviders } from "../../utils/Renderwithproviders";
jest.mock("../../../src/utils/apiUtils");
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
let apiGetSpy: jest.SpyInstance;
@@ -315,6 +317,9 @@ describe("Timezone Logic - Backend to Frontend Flow", () => {
});
describe("User Actions - Changing Timezone", () => {
beforeEach(() => {
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
});
test("User enables browser default => Settings gets browser TZ, User gets null", async () => {
const { store } = renderWithProviders(<SettingsPage />, {
user: {
@@ -24,12 +24,17 @@ describe("SettingsPage", () => {
},
organiserData: null,
tokens: null,
coreConfig: { language: "en" },
coreConfig: {
language: "en",
datetime: { timeZone: "UTC" },
},
loading: false,
error: null,
},
settings: {
language: "en",
timeZone: "UTC",
isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -110,12 +115,14 @@ describe("SettingsPage", () => {
},
organiserData: null,
tokens: null,
coreConfig: { language: "fr" },
coreConfig: { language: "fr", datetime: { timeZone: "UTC" } },
loading: false,
error: null,
},
settings: {
language: "en",
timeZone: "UTC",
isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -260,6 +267,9 @@ describe("SettingsPage", () => {
});
describe("Timezone Settings", () => {
beforeEach(() => {
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
});
it("displays timezone selector in Settings tab", () => {
renderWithProviders(<SettingsPage />, preloadedState);
@@ -289,6 +299,7 @@ describe("SettingsPage", () => {
settings: {
language: "en",
timeZone: "UTC",
isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -298,7 +309,7 @@ describe("SettingsPage", () => {
expect(screen.getByDisplayValue(/America\/New York/i)).toBeDefined();
});
it("updates timezone immediately (optimistic update)", async () => {
it("updates timezone immediately (optimistic update) and calls API", async () => {
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
const timezoneInput = screen.getAllByRole("combobox")[1];
@@ -319,6 +330,27 @@ describe("SettingsPage", () => {
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 () => {
@@ -340,7 +372,30 @@ describe("SettingsPage", () => {
expect(state.settings.timeZone).toBe("Asia/Tokyo");
});
// Test with 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: "Asia/Tokyo",
}),
}),
]),
}),
]),
})
);
});
jest.clearAllMocks();
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
fireEvent.change(timezoneInput, {
target: { value: "America/Los_Angeles" },
});
@@ -357,6 +412,172 @@ describe("SettingsPage", () => {
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(<SettingsPage />, 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(<SettingsPage />, 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(<SettingsPage />, 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(<SettingsPage />, 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", () => {
@@ -379,6 +600,7 @@ describe("SettingsPage", () => {
settings: {
language: "en",
timeZone: undefined,
isBrowserDefaultTimeZone: false,
view: "settings",
},
};
@@ -388,5 +610,53 @@ describe("SettingsPage", () => {
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(<SettingsPage />, 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,
},
}),
]),
}),
]),
})
);
});
});
});
});
+30 -27
View File
@@ -47,7 +47,7 @@ type SettingsSubTab = "settings" | "notifications";
export default function SettingsPage() {
const dispatch = useAppDispatch();
const { t } = useI18n();
// const previousConfig = useAppSelector((state) => state.user.coreConfig);
const previousConfig = useAppSelector((state) => state.user.coreConfig);
const userLanguage = useAppSelector(
(state) => state.user?.coreConfig.language
);
@@ -115,46 +115,49 @@ export default function SettingsPage() {
};
const handleTimeZoneChange = (newTimeZone: string) => {
// const previousTimeZone = currentTimeZone;
const previousTimeZone = currentTimeZone;
// Optimistic update - update UI immediately
dispatch(setUserTimeZone(newTimeZone));
dispatch(setSettingsTimeZone(newTimeZone));
// // Call API in background, don't wait for it
// dispatch(
// updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
// )
// .unwrap()
// .catch((error) => {
// console.error("Failed to update TimeZone:", error);
// // Rollback on error
// dispatch(setUserTimeZone(previousTimeZone));
// dispatch(setSettingsTimeZone(previousTimeZone));
// setTimeZoneErrorOpen(true);
// });
// Call API in background, don't wait for it
dispatch(
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
)
.unwrap()
.catch((error) => {
console.error("Failed to update TimeZone:", error);
// Rollback on error
dispatch(setUserTimeZone(previousTimeZone));
dispatch(setSettingsTimeZone(previousTimeZone));
setTimeZoneErrorOpen(true);
});
};
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
const previousTimeZone = currentTimeZone;
// Optimistic update - update UI immediately
dispatch(setIsBrowserDefaultTimeZone(isDefault));
if (isDefault) {
dispatch(setUserTimeZone(null));
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
}
// // Call API in background, don't wait for it
// dispatch(
// updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
// )
// .unwrap()
// .catch((error) => {
// console.error("Failed to update TimeZone:", error);
// // Rollback on error
// dispatch(setUserTimeZone(previousTimeZone));
// dispatch(setSettingsTimeZone(previousTimeZone));
// setTimeZoneErrorOpen(true);
// });
// Call API in background, don't wait for it
dispatch(
updateUserConfigurationsAsync({ timezone: null, previousConfig })
)
.unwrap()
.catch((error) => {
console.error("Failed to update TimeZone:", error);
// Rollback on error
dispatch(setUserTimeZone(previousTimeZone));
dispatch(setSettingsTimeZone(previousTimeZone));
dispatch(setIsBrowserDefaultTimeZone(!isDefault));
setTimeZoneErrorOpen(true);
});
}
};
const handleTimeZoneErrorClose = () => {
+1 -1
View File
@@ -37,7 +37,7 @@ export async function getUserDetails(id: string) {
export interface UserConfigurationUpdates {
language?: string;
notifications?: Record<string, unknown>;
timezone?: string;
timezone?: string | null;
previousConfig?: Record<string, any>;
}