Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user