Phase 2: Language Context & Selector - Add language dropdown to Header and auth pages
- Create LanguageSelector component with dropdown (shows flag + name) - Add LanguageSelector to Header (right side, near user email/logout) - Add LanguageSelector to login, signup, and signup/[code] pages - Create test-utils.tsx with renderWithProviders helper - Add vitest.setup.ts to mock localStorage - Update all test files to use renderWithProviders - Language selector persists choice in localStorage - HTML lang attribute updates dynamically based on selected language All frontend and e2e tests passing.
This commit is contained in:
parent
f7553df05d
commit
f86ec8b62d
11 changed files with 214 additions and 37 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { expect, test, vi, beforeEach, afterEach, describe } from "vitest";
|
||||
import ProfilePage from "./page";
|
||||
import { renderWithProviders } from "../test-utils";
|
||||
|
||||
// Mock next/navigation
|
||||
const mockPush = vi.fn();
|
||||
|
|
@ -37,6 +38,7 @@ vi.mock("../auth-context", () => ({
|
|||
hasRole: mockHasRole,
|
||||
hasPermission: mockHasPermission,
|
||||
}),
|
||||
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
Permission: {
|
||||
VIEW_AUDIT: "view_audit",
|
||||
FETCH_PRICE: "fetch_price",
|
||||
|
|
@ -112,14 +114,14 @@ describe("ProfilePage - Display", () => {
|
|||
mockIsLoading = true;
|
||||
mockGetProfile.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
expect(screen.getByText("Loading...")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders profile page title", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
|
@ -128,7 +130,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("displays login email as read-only", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
const loginEmailInput = screen.getByDisplayValue("test@example.com");
|
||||
expect(loginEmailInput).toBeDefined();
|
||||
|
|
@ -139,7 +141,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("shows read-only badge for login email", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Read only")).toBeDefined();
|
||||
});
|
||||
|
|
@ -148,7 +150,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("shows hint about login email", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/cannot be changed/i)).toBeDefined();
|
||||
});
|
||||
|
|
@ -157,7 +159,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("displays contact details section hint", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/communication purposes only/i)).toBeDefined();
|
||||
});
|
||||
|
|
@ -166,7 +168,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("displays fetched profile data", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("contact@example.com")).toBeDefined();
|
||||
expect(screen.getByDisplayValue("@testuser")).toBeDefined();
|
||||
|
|
@ -178,7 +180,7 @@ describe("ProfilePage - Display", () => {
|
|||
test("fetches profile with credentials", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetProfile).toHaveBeenCalled();
|
||||
|
|
@ -193,7 +195,7 @@ describe("ProfilePage - Display", () => {
|
|||
nostr_npub: null,
|
||||
});
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
// Check that inputs exist with empty values (placeholders shown)
|
||||
const telegramInput = document.getElementById("telegram") as HTMLInputElement;
|
||||
|
|
@ -206,7 +208,7 @@ describe("ProfilePage - Navigation", () => {
|
|||
test("shows nav links for regular user", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Exchange")).toBeDefined();
|
||||
expect(screen.getByText("My Trades")).toBeDefined();
|
||||
|
|
@ -216,7 +218,7 @@ describe("ProfilePage - Navigation", () => {
|
|||
test("highlights My Profile in nav", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
// My Profile should be visible (as current page indicator)
|
||||
const navItems = screen.getAllByText("My Profile");
|
||||
|
|
@ -229,7 +231,7 @@ describe("ProfilePage - Access Control", () => {
|
|||
test("redirects to login when not authenticated", async () => {
|
||||
mockUser = null;
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith("/login");
|
||||
|
|
@ -244,7 +246,7 @@ describe("ProfilePage - Access Control", () => {
|
|||
permissions: ["view_all_exchanges"],
|
||||
};
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith("/admin/trades");
|
||||
|
|
@ -259,7 +261,7 @@ describe("ProfilePage - Access Control", () => {
|
|||
permissions: ["view_audit"],
|
||||
};
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
// Give it a moment to potentially fetch
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
|
@ -272,7 +274,7 @@ describe("ProfilePage - Loading State", () => {
|
|||
mockIsLoading = true;
|
||||
mockUser = null;
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -280,7 +282,7 @@ describe("ProfilePage - Loading State", () => {
|
|||
test("shows loading indicator while loading", () => {
|
||||
mockIsLoading = true;
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
expect(screen.getByText("Loading...")).toBeDefined();
|
||||
});
|
||||
|
|
@ -290,7 +292,7 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
test("submit button is disabled when no changes", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
await waitFor(() => {
|
||||
const submitButton = screen.getByRole("button", { name: /save changes/i });
|
||||
expect(submitButton).toHaveProperty("disabled", true);
|
||||
|
|
@ -300,7 +302,7 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
test("submit button is enabled after field changes", async () => {
|
||||
mockGetProfile.mockResolvedValue(mockProfileData);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("@testuser")).toBeDefined();
|
||||
|
|
@ -323,7 +325,7 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
nostr_npub: null,
|
||||
});
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
@ -345,7 +347,7 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
nostr_npub: null,
|
||||
});
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
@ -382,7 +384,7 @@ describe("ProfilePage - Form Submission", () => {
|
|||
nostr_npub: null,
|
||||
});
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
@ -428,7 +430,7 @@ describe("ProfilePage - Form Submission", () => {
|
|||
})
|
||||
);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
@ -456,7 +458,7 @@ describe("ProfilePage - Form Submission", () => {
|
|||
});
|
||||
mockUpdateProfile.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
@ -499,7 +501,7 @@ describe("ProfilePage - Form Submission", () => {
|
|||
});
|
||||
mockUpdateProfile.mockReturnValue(submitPromise);
|
||||
|
||||
render(<ProfilePage />);
|
||||
renderWithProviders(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue