diff --git a/frontend/app/profile/page.test.tsx b/frontend/app/profile/page.test.tsx
index 61957c8..ae1d4e5 100644
--- a/frontend/app/profile/page.test.tsx
+++ b/frontend/app/profile/page.test.tsx
@@ -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(
);
+ renderWithProviders(
);
expect(screen.getByText("Loading...")).toBeDefined();
});
test("renders profile page title", async () => {
mockGetProfile.mockResolvedValue(mockProfileData);
- render(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(mockGetProfile).toHaveBeenCalled();
@@ -193,7 +195,7 @@ describe("ProfilePage - Display", () => {
nostr_npub: null,
});
- render(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith("/login");
@@ -244,7 +246,7 @@ describe("ProfilePage - Access Control", () => {
permissions: ["view_all_exchanges"],
};
- render(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith("/admin/trades");
@@ -259,7 +261,7 @@ describe("ProfilePage - Access Control", () => {
permissions: ["view_audit"],
};
- render(
);
+ renderWithProviders(
);
// 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(
);
+ renderWithProviders(
);
expect(mockPush).not.toHaveBeenCalled();
});
@@ -280,7 +282,7 @@ describe("ProfilePage - Loading State", () => {
test("shows loading indicator while loading", () => {
mockIsLoading = true;
- render(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByDisplayValue("@testuser")).toBeDefined();
@@ -323,7 +325,7 @@ describe("ProfilePage - Form Behavior", () => {
nostr_npub: null,
});
- render(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
@@ -345,7 +347,7 @@ describe("ProfilePage - Form Behavior", () => {
nostr_npub: null,
});
- render(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
@@ -382,7 +384,7 @@ describe("ProfilePage - Form Submission", () => {
nostr_npub: null,
});
- render(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
@@ -428,7 +430,7 @@ describe("ProfilePage - Form Submission", () => {
})
);
- render(
);
+ renderWithProviders(
);
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(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
@@ -499,7 +501,7 @@ describe("ProfilePage - Form Submission", () => {
});
mockUpdateProfile.mockReturnValue(submitPromise);
- render(
);
+ renderWithProviders(
);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
diff --git a/frontend/app/signup/[code]/page.tsx b/frontend/app/signup/[code]/page.tsx
index 065d2a4..0505e56 100644
--- a/frontend/app/signup/[code]/page.tsx
+++ b/frontend/app/signup/[code]/page.tsx
@@ -3,6 +3,7 @@
import { useEffect } from "react";
import { useRouter, useParams } from "next/navigation";
import { useAuth } from "../../auth-context";
+import { LanguageSelector } from "../../components/LanguageSelector";
export default function SignupWithCodePage() {
const params = useParams();
@@ -36,6 +37,9 @@ export default function SignupWithCodePage() {
fontFamily: "'DM Sans', system-ui, sans-serif",
}}
>
+
+
+
Redirecting...
);
diff --git a/frontend/app/signup/page.test.tsx b/frontend/app/signup/page.test.tsx
index b25d956..f4aec2a 100644
--- a/frontend/app/signup/page.test.tsx
+++ b/frontend/app/signup/page.test.tsx
@@ -1,6 +1,7 @@
-import { render, screen, cleanup } from "@testing-library/react";
+import { screen, cleanup } from "@testing-library/react";
import { expect, test, vi, beforeEach, afterEach } from "vitest";
import SignupPage from "./page";
+import { renderWithProviders } from "../test-utils";
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
@@ -8,30 +9,32 @@ vi.mock("next/navigation", () => ({
useSearchParams: () => ({ get: () => null }),
}));
+const mockRegister = vi.fn();
vi.mock("../auth-context", () => ({
- useAuth: () => ({ user: null, register: vi.fn() }),
+ useAuth: () => ({ user: null, register: mockRegister }),
+ AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
}));
beforeEach(() => vi.clearAllMocks());
afterEach(() => cleanup());
test("renders signup form with title", () => {
- render(
);
+ renderWithProviders(
);
// Step 1 shows "Join with Invite" title (invite code entry)
expect(screen.getByRole("heading", { name: "Join with Invite" })).toBeDefined();
});
test("renders invite code input", () => {
- render(
);
+ renderWithProviders(
);
expect(screen.getByLabelText("Invite Code")).toBeDefined();
});
test("renders continue button", () => {
- render(
);
+ renderWithProviders(
);
expect(screen.getByRole("button", { name: "Continue" })).toBeDefined();
});
test("renders link to login", () => {
- render(
);
+ renderWithProviders(
);
expect(screen.getByText("Sign in")).toBeDefined();
});
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx
index c08dcb6..e9c4717 100644
--- a/frontend/app/signup/page.tsx
+++ b/frontend/app/signup/page.tsx
@@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "../auth-context";
import { invitesApi } from "../api";
import { authFormStyles as styles } from "../styles/auth-form";
+import { LanguageSelector } from "../components/LanguageSelector";
function SignupContent() {
const searchParams = useSearchParams();
@@ -107,6 +108,9 @@ function SignupContent() {
if (isCheckingInitialCode) {
return (
+
+
+
@@ -122,6 +126,9 @@ function SignupContent() {
if (!inviteValid) {
return (
+
+
+
@@ -189,6 +196,9 @@ function SignupContent() {
// Step 2: Enter email and password
return (
+
+
+
diff --git a/frontend/app/test-utils.tsx b/frontend/app/test-utils.tsx
new file mode 100644
index 0000000..2b7ddde
--- /dev/null
+++ b/frontend/app/test-utils.tsx
@@ -0,0 +1,16 @@
+import { ReactElement } from "react";
+import { render, RenderOptions } from "@testing-library/react";
+import { Providers } from "./components/Providers";
+import { AuthProvider } from "./auth-context";
+
+function AllProviders({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function renderWithProviders(ui: ReactElement, options?: Omit
) {
+ return render(ui, { wrapper: AllProviders, ...options });
+}
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 1cc37d3..40727b8 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -6,6 +6,7 @@ export default defineConfig({
test: {
environment: "jsdom",
include: ["app/**/*.test.{ts,tsx}"],
+ setupFiles: ["./vitest.setup.ts"],
coverage: {
provider: "v8",
reporter: ["text", "html"],
diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts
new file mode 100644
index 0000000..3275f4d
--- /dev/null
+++ b/frontend/vitest.setup.ts
@@ -0,0 +1,27 @@
+import { beforeEach } from "vitest";
+
+// Mock localStorage
+const localStorageMock = (() => {
+ let store: Record = {};
+
+ return {
+ getItem: (key: string) => store[key] || null,
+ setItem: (key: string, value: string) => {
+ store[key] = value.toString();
+ },
+ removeItem: (key: string) => {
+ delete store[key];
+ },
+ clear: () => {
+ store = {};
+ },
+ };
+})();
+
+Object.defineProperty(window, "localStorage", {
+ value: localStorageMock,
+});
+
+beforeEach(() => {
+ localStorageMock.clear();
+});