* feat: #333 load language from backend configuration * feat(settings): sync language with user configs api * Update __test__/features/Settings/SettingsPage.test.tsx Co-authored-by: Lê Nhân Phụng <lenhanphung@Phung-Mac-M4.local> Co-authored-by: Benoit TELLIER <btellier@linagora.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2,8 +2,16 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import SettingsPage from "../../../src/features/Settings/SettingsPage";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
import { api } from "../../../src/utils/apiUtils";
|
||||
|
||||
jest.mock("../../../src/utils/apiUtils");
|
||||
|
||||
describe("SettingsPage", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
@@ -14,6 +22,11 @@ describe("SettingsPage", () => {
|
||||
sid: "mockSid",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
},
|
||||
organiserData: null,
|
||||
tokens: null,
|
||||
language: "en",
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
@@ -82,49 +95,95 @@ describe("SettingsPage", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays all available language options", async () => {
|
||||
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,
|
||||
language: "fr",
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
settings: {
|
||||
language: "en",
|
||||
view: "settings",
|
||||
},
|
||||
};
|
||||
renderWithProviders(<SettingsPage />, stateWithUserLanguage);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// Verify Select exists
|
||||
expect(languageSelect).toBeInTheDocument();
|
||||
|
||||
// Verify that the underlying native input reflects the user language ("fr")
|
||||
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(<SettingsPage />, preloadedState);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// MUI Select uses a native input element - find and change it
|
||||
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?.language).toBe("fr");
|
||||
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(<SettingsPage />, preloadedState);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// Click on the select to open dropdown
|
||||
fireEvent.mouseDown(languageSelect);
|
||||
|
||||
// Wait for menu to appear - MUI Select uses Menu internally
|
||||
// Note: In test environment, Select may not open menu, so we verify Select exists and has correct value
|
||||
expect(languageSelect).toBeInTheDocument();
|
||||
expect(languageSelect).toHaveTextContent("English");
|
||||
});
|
||||
|
||||
it("dispatches setLanguage action when language is changed", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// MUI Select uses a native input element - find and change it
|
||||
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" } });
|
||||
|
||||
await waitFor(() => {
|
||||
const state = store.getState();
|
||||
expect(state.settings.language).toBe("fr");
|
||||
});
|
||||
});
|
||||
|
||||
it("saves language change to localStorage", async () => {
|
||||
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// MUI Select uses a native input element - find and change it
|
||||
const nativeInput = languageSelect.querySelector(
|
||||
'input[aria-hidden="true"]'
|
||||
@@ -138,11 +197,43 @@ describe("SettingsPage", () => {
|
||||
});
|
||||
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(<SettingsPage />, preloadedState);
|
||||
|
||||
const languageSelect = screen.getByLabelText("settings.languageSelector");
|
||||
|
||||
// MUI Select uses a native input element - find and change it
|
||||
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?.language).toBe("en");
|
||||
expect(state.settings.language).toBe("en");
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("shows empty state in Notifications tab", () => {
|
||||
renderWithProviders(<SettingsPage />, preloadedState);
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { clientConfig } from "../../../src/features/User/oidcAuth";
|
||||
import { getOpenPaasUser } from "../../../src/features/User/userAPI";
|
||||
import {
|
||||
getOpenPaasUser,
|
||||
updateUserConfigurations,
|
||||
} from "../../../src/features/User/userAPI";
|
||||
import { api } from "../../../src/utils/apiUtils";
|
||||
|
||||
jest.mock("../../../src/utils/apiUtils");
|
||||
|
||||
clientConfig.url = "https://example.com";
|
||||
|
||||
describe("getOpenPaasUserId", () => {
|
||||
describe("getOpenPaasUser", () => {
|
||||
it("should fetch and return user data", async () => {
|
||||
const mockUser = { id: "123", name: "OpenPaas User" };
|
||||
|
||||
@@ -20,3 +23,54 @@ describe("getOpenPaasUserId", () => {
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateUserConfigurations", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should PATCH configurations with language update", async () => {
|
||||
const mockResponse = { status: 204 };
|
||||
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await updateUserConfigurations({ language: "vi" });
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||
json: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [{ name: "language", value: "vi" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should PATCH configurations with multiple updates", async () => {
|
||||
const mockResponse = { status: 204 };
|
||||
(api.patch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await updateUserConfigurations({
|
||||
language: "fr",
|
||||
timezone: "Europe/Paris",
|
||||
});
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith("api/configurations?scope=user", {
|
||||
json: [
|
||||
{
|
||||
name: "core",
|
||||
configurations: [
|
||||
{ name: "language", value: "fr" },
|
||||
{ name: "timezone", value: "Europe/Paris" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty updates without calling API", async () => {
|
||||
const result = await updateUserConfigurations({});
|
||||
|
||||
expect(api.patch).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ status: 204 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user