UI: #334 add config page language setting (#374)

* UI: #334 add config page language setting

* temporary remove logout button

* Unify fullscreen view class for dialog expanded and settings view

- Replace dialog-expanded and settings-view with single fullscreen-view class
- Update ResponsiveDialog to use fullscreen-view class
- Update CalendarLayout to use fullscreen-view class for settings view
- Consolidate CSS rules in Menubar.styl to use fullscreen-view
- Update test cases to use new class name

---------

Co-authored-by: Lê Nhân Phụng <lenhanphung@Phung-Mac-M4.local>
This commit is contained in:
lenhanphung
2025-11-28 20:25:06 +07:00
committed by GitHub
parent 6fcf22a1cf
commit 1a85afc57d
15 changed files with 801 additions and 58 deletions
+108 -10
View File
@@ -409,7 +409,7 @@ describe("Menubar interaction with expanded Dialog", () => {
});
afterEach(() => {
document.body.classList.remove("dialog-expanded");
document.body.classList.remove("fullscreen-view");
});
it("has navigation controls element with correct class", () => {
@@ -497,7 +497,7 @@ describe("Menubar interaction with expanded Dialog", () => {
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded");
document.body.classList.add("fullscreen-view");
renderWithProviders(
<Menubar
@@ -519,7 +519,7 @@ describe("Menubar interaction with expanded Dialog", () => {
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded");
document.body.classList.add("fullscreen-view");
renderWithProviders(
<Menubar
@@ -538,13 +538,11 @@ describe("Menubar interaction with expanded Dialog", () => {
expect(screen.getByText("Twake")).toBeInTheDocument();
});
it("keeps avatar clickable when dialog is expanded", async () => {
it("opens user dropdown menu when clicking avatar", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
@@ -561,10 +559,110 @@ describe("Menubar interaction with expanded Dialog", () => {
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
const languageSelector = screen.getByLabelText(
"menubar.languageSelector"
);
expect(languageSelector).toBeInTheDocument();
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("test@test.com")).toBeInTheDocument();
});
});
it("closes user dropdown menu when clicking outside", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
// Click outside the menu - MUI Menu closes on backdrop click
const backdrop = document.querySelector(".MuiBackdrop-root");
if (backdrop) {
fireEvent.click(backdrop);
} else {
// Fallback: click on body
fireEvent.mouseDown(document.body);
}
await waitFor(
() => {
expect(screen.queryByText("John Doe")).not.toBeInTheDocument();
},
{ timeout: 2000 }
);
});
it("displays user name and email in dropdown menu", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("test@test.com")).toBeInTheDocument();
});
});
it("navigates to settings page when clicking Settings in dropdown", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
const preloadedStateWithSettings = {
...preloadedState,
settings: {
language: "en",
view: "calendar",
},
};
const { store } = renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedStateWithSettings
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText(/Settings/i)).toBeInTheDocument();
});
const settingsButton = screen.getByText(/Settings/i);
fireEvent.click(settingsButton);
await waitFor(() => {
const state = store.getState();
expect(state.settings.view).toBe("settings");
});
});
@@ -0,0 +1,158 @@
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";
describe("SettingsPage", () => {
const preloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
family_name: "Doe",
name: "John",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
},
settings: {
language: "en",
view: "settings",
},
};
it("renders settings page with sidebar and content layout", () => {
renderWithProviders(<SettingsPage />, preloadedState);
expect(screen.getByRole("main")).toHaveClass(
"main-layout",
"settings-layout"
);
});
it("displays sidebar navigation with Settings and Notifications items", () => {
const { container } = renderWithProviders(<SettingsPage />, preloadedState);
// Check sidebar navigation items
const sidebar = container.querySelector(".settings-sidebar");
expect(sidebar).toBeInTheDocument();
expect(screen.getAllByText(/settings.title/i).length).toBeGreaterThan(0);
expect(screen.getByText(/settings.notifications/i)).toBeInTheDocument();
});
it("highlights active navigation item", () => {
const { container } = renderWithProviders(<SettingsPage />, preloadedState);
const settingsNavItem = container.querySelector(
".settings-nav-item.active"
);
expect(settingsNavItem).toBeInTheDocument();
});
it("navigates back to calendar view when clicking back button", async () => {
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
const backButton = screen.getByLabelText("settings.back");
fireEvent.click(backButton);
await waitFor(() => {
const state = store.getState();
expect(state.settings.view).toBe("calendar");
});
});
it("switches between Settings and Notifications tabs", () => {
renderWithProviders(<SettingsPage />, preloadedState);
const notificationsTab = screen.getByRole("tab", {
name: /settings.notifications/i,
});
fireEvent.click(notificationsTab);
expect(
screen.getByText(/settings.notifications.empty/i)
).toBeInTheDocument();
});
it("displays language select in Settings tab", () => {
renderWithProviders(<SettingsPage />, preloadedState);
expect(
screen.getByLabelText("settings.languageSelector")
).toBeInTheDocument();
});
it("displays all available language options", async () => {
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"]'
) 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(() => {
expect(localStorage.getItem("lang")).toBe("fr");
});
});
it("shows empty state in Notifications tab", () => {
renderWithProviders(<SettingsPage />, preloadedState);
const notificationsTab = screen.getByRole("tab", {
name: /settings.notifications/i,
});
fireEvent.click(notificationsTab);
expect(
screen.getByText("settings.notifications.empty")
).toBeInTheDocument();
});
});
+92
View File
@@ -0,0 +1,92 @@
import { getUserDisplayName } from "../../src/utils/userUtils";
import { userData } from "../../src/features/User/userDataTypes";
describe("userUtils", () => {
describe("getUserDisplayName", () => {
it("returns full name when user has name and family_name", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "John",
name: "John",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("John Doe");
});
it("returns email when user does not have name and family_name", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
it("returns email when user has only email", () => {
const user: userData = {
sub: "test",
email: "user@example.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("user@example.com");
});
it("returns empty string when user is null", () => {
expect(getUserDisplayName(null)).toBe("");
});
it("returns empty string when user is undefined", () => {
expect(getUserDisplayName(undefined)).toBe("");
});
it("returns empty string when user has no name, family_name, or email", () => {
const user: userData = {
sub: "test",
email: "",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("");
});
it("handles user with only name (no family_name)", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "John",
name: "John",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
it("handles user with only family_name (no name)", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
});
});