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 { browserDefaultTimeZone } from "../../../src/utils/timezone";
|
||||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
|
jest.mock("../../../src/utils/apiUtils");
|
||||||
|
|
||||||
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
|
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
|
||||||
let apiGetSpy: jest.SpyInstance;
|
let apiGetSpy: jest.SpyInstance;
|
||||||
|
|
||||||
@@ -315,6 +317,9 @@ describe("Timezone Logic - Backend to Frontend Flow", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("User Actions - Changing Timezone", () => {
|
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 () => {
|
test("User enables browser default => Settings gets browser TZ, User gets null", async () => {
|
||||||
const { store } = renderWithProviders(<SettingsPage />, {
|
const { store } = renderWithProviders(<SettingsPage />, {
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
@@ -24,12 +24,17 @@ describe("SettingsPage", () => {
|
|||||||
},
|
},
|
||||||
organiserData: null,
|
organiserData: null,
|
||||||
tokens: null,
|
tokens: null,
|
||||||
coreConfig: { language: "en" },
|
coreConfig: {
|
||||||
|
language: "en",
|
||||||
|
datetime: { timeZone: "UTC" },
|
||||||
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
language: "en",
|
language: "en",
|
||||||
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
view: "settings",
|
view: "settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -110,12 +115,14 @@ describe("SettingsPage", () => {
|
|||||||
},
|
},
|
||||||
organiserData: null,
|
organiserData: null,
|
||||||
tokens: null,
|
tokens: null,
|
||||||
coreConfig: { language: "fr" },
|
coreConfig: { language: "fr", datetime: { timeZone: "UTC" } },
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
language: "en",
|
language: "en",
|
||||||
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
view: "settings",
|
view: "settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -260,6 +267,9 @@ describe("SettingsPage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Timezone Settings", () => {
|
describe("Timezone Settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
});
|
||||||
it("displays timezone selector in Settings tab", () => {
|
it("displays timezone selector in Settings tab", () => {
|
||||||
renderWithProviders(<SettingsPage />, preloadedState);
|
renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
@@ -289,6 +299,7 @@ describe("SettingsPage", () => {
|
|||||||
settings: {
|
settings: {
|
||||||
language: "en",
|
language: "en",
|
||||||
timeZone: "UTC",
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
view: "settings",
|
view: "settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -298,7 +309,7 @@ describe("SettingsPage", () => {
|
|||||||
expect(screen.getByDisplayValue(/America\/New York/i)).toBeDefined();
|
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 { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
const timezoneInput = screen.getAllByRole("combobox")[1];
|
const timezoneInput = screen.getAllByRole("combobox")[1];
|
||||||
@@ -319,6 +330,27 @@ describe("SettingsPage", () => {
|
|||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.settings.timeZone).toBe("Europe/Paris");
|
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 () => {
|
it("handles timezone change with different timezone values", async () => {
|
||||||
@@ -340,7 +372,30 @@ describe("SettingsPage", () => {
|
|||||||
expect(state.settings.timeZone).toBe("Asia/Tokyo");
|
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, {
|
fireEvent.change(timezoneInput, {
|
||||||
target: { value: "America/Los_Angeles" },
|
target: { value: "America/Los_Angeles" },
|
||||||
});
|
});
|
||||||
@@ -357,6 +412,172 @@ describe("SettingsPage", () => {
|
|||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.settings.timeZone).toBe("America/Los_Angeles");
|
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", () => {
|
it("uses UTC as default timezone when no timezone is set", () => {
|
||||||
@@ -379,6 +600,7 @@ describe("SettingsPage", () => {
|
|||||||
settings: {
|
settings: {
|
||||||
language: "en",
|
language: "en",
|
||||||
timeZone: undefined,
|
timeZone: undefined,
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
view: "settings",
|
view: "settings",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -388,5 +610,53 @@ describe("SettingsPage", () => {
|
|||||||
const timezoneInput = screen.getByDisplayValue("(UTC) UTC");
|
const timezoneInput = screen.getByDisplayValue("(UTC) UTC");
|
||||||
expect(timezoneInput).toBeDefined();
|
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() {
|
export default function SettingsPage() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
// const previousConfig = useAppSelector((state) => state.user.coreConfig);
|
const previousConfig = useAppSelector((state) => state.user.coreConfig);
|
||||||
const userLanguage = useAppSelector(
|
const userLanguage = useAppSelector(
|
||||||
(state) => state.user?.coreConfig.language
|
(state) => state.user?.coreConfig.language
|
||||||
);
|
);
|
||||||
@@ -115,46 +115,49 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeZoneChange = (newTimeZone: string) => {
|
const handleTimeZoneChange = (newTimeZone: string) => {
|
||||||
// const previousTimeZone = currentTimeZone;
|
const previousTimeZone = currentTimeZone;
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
// Optimistic update - update UI immediately
|
||||||
dispatch(setUserTimeZone(newTimeZone));
|
dispatch(setUserTimeZone(newTimeZone));
|
||||||
dispatch(setSettingsTimeZone(newTimeZone));
|
dispatch(setSettingsTimeZone(newTimeZone));
|
||||||
|
|
||||||
// // Call API in background, don't wait for it
|
// Call API in background, don't wait for it
|
||||||
// dispatch(
|
dispatch(
|
||||||
// updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
|
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
|
||||||
// )
|
)
|
||||||
// .unwrap()
|
.unwrap()
|
||||||
// .catch((error) => {
|
.catch((error) => {
|
||||||
// console.error("Failed to update TimeZone:", error);
|
console.error("Failed to update TimeZone:", error);
|
||||||
// // Rollback on error
|
// Rollback on error
|
||||||
// dispatch(setUserTimeZone(previousTimeZone));
|
dispatch(setUserTimeZone(previousTimeZone));
|
||||||
// dispatch(setSettingsTimeZone(previousTimeZone));
|
dispatch(setSettingsTimeZone(previousTimeZone));
|
||||||
// setTimeZoneErrorOpen(true);
|
setTimeZoneErrorOpen(true);
|
||||||
// });
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
|
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
|
||||||
|
const previousTimeZone = currentTimeZone;
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
// Optimistic update - update UI immediately
|
||||||
dispatch(setIsBrowserDefaultTimeZone(isDefault));
|
dispatch(setIsBrowserDefaultTimeZone(isDefault));
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
dispatch(setUserTimeZone(null));
|
dispatch(setUserTimeZone(null));
|
||||||
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
|
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
|
||||||
}
|
|
||||||
|
|
||||||
// // Call API in background, don't wait for it
|
// Call API in background, don't wait for it
|
||||||
// dispatch(
|
dispatch(
|
||||||
// updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
|
updateUserConfigurationsAsync({ timezone: null, previousConfig })
|
||||||
// )
|
)
|
||||||
// .unwrap()
|
.unwrap()
|
||||||
// .catch((error) => {
|
.catch((error) => {
|
||||||
// console.error("Failed to update TimeZone:", error);
|
console.error("Failed to update TimeZone:", error);
|
||||||
// // Rollback on error
|
// Rollback on error
|
||||||
// dispatch(setUserTimeZone(previousTimeZone));
|
dispatch(setUserTimeZone(previousTimeZone));
|
||||||
// dispatch(setSettingsTimeZone(previousTimeZone));
|
dispatch(setSettingsTimeZone(previousTimeZone));
|
||||||
// setTimeZoneErrorOpen(true);
|
dispatch(setIsBrowserDefaultTimeZone(!isDefault));
|
||||||
// });
|
setTimeZoneErrorOpen(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeZoneErrorClose = () => {
|
const handleTimeZoneErrorClose = () => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function getUserDetails(id: string) {
|
|||||||
export interface UserConfigurationUpdates {
|
export interface UserConfigurationUpdates {
|
||||||
language?: string;
|
language?: string;
|
||||||
notifications?: Record<string, unknown>;
|
notifications?: Record<string, unknown>;
|
||||||
timezone?: string;
|
timezone?: string | null;
|
||||||
previousConfig?: Record<string, any>;
|
previousConfig?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user