diff --git a/frontend/src/app/[locale]/settings/page.test.tsx b/frontend/src/app/[locale]/settings/page.test.tsx new file mode 100644 index 00000000..7cc6e680 --- /dev/null +++ b/frontend/src/app/[locale]/settings/page.test.tsx @@ -0,0 +1,82 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import SettingsPage from "./page"; + +jest.mock("../../lib/session", () => ({ + logoutUser: jest.fn(), +})); + +jest.mock("../../hooks/useLogout", () => ({ + useLogout: () => ({ logout: jest.fn() }), +})); + +jest.mock("../../stores/useUserStore", () => ({ + useUserStore: jest.fn((selector) => + selector({ + user: { id: "user1", email: "test@example.com" }, + }), + ), + selectUser: (state: { user: { id: string; email: string } }) => state.user, +})); + +jest.mock("../../stores/useWalletStore", () => ({ + useWalletStore: jest.fn((selector) => + selector({ + address: null, + network: "testnet", + disconnect: jest.fn(), + }), + ), + selectWalletAddress: (state: { address: string | null }) => state.address, + selectWalletNetwork: (state: { network: string }) => state.network, +})); + +jest.mock("../../stores/useThemeStore", () => ({ + useThemeStore: jest.fn(() => ({ + theme: "system", + setTheme: jest.fn(), + })), +})); + +jest.mock("../../hooks/useApi", () => ({ + useNotificationPreferences: () => ({ data: undefined, isLoading: false, error: null }), + useUpdateNotificationPreferences: () => ({ mutate: jest.fn(), isPending: false }), +})); + +jest.mock("../../components/gamification/GamificationSettings", () => ({ + GamificationSettings: () =>
Gamification Settings
, +})); + +describe("SettingsPage section navigation", () => { + it("exposes the default active section via aria-selected", () => { + render(); + + expect(screen.getByRole("tab", { name: "Profile" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("tab", { name: "Wallet" })).toHaveAttribute("aria-selected", "false"); + }); + + it("updates accessible state and focus when switching sections", async () => { + const user = userEvent.setup(); + render(); + + const walletTab = screen.getByRole("tab", { name: "Wallet" }); + await user.click(walletTab); + + expect(walletTab).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("tab", { name: "Profile" })).toHaveAttribute("aria-selected", "false"); + expect(document.activeElement).toBe(walletTab); + }); + + it("links each tab to its panel with aria-controls and tabpanel semantics", () => { + render(); + + const profileTab = screen.getByRole("tab", { name: "Profile" }); + const panelId = profileTab.getAttribute("aria-controls"); + + expect(panelId).toBe("settings-panel-profile"); + + const panel = screen.getByRole("tabpanel"); + expect(panel).toHaveAttribute("id", panelId); + expect(panel).toHaveAttribute("aria-labelledby", "settings-tab-profile"); + }); +}); diff --git a/frontend/src/app/[locale]/settings/page.tsx b/frontend/src/app/[locale]/settings/page.tsx index b4926b38..57d6c47b 100644 --- a/frontend/src/app/[locale]/settings/page.tsx +++ b/frontend/src/app/[locale]/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, type KeyboardEvent } from "react"; import { User, Wallet, @@ -55,6 +55,14 @@ const SECTIONS = [ type SectionId = (typeof SECTIONS)[number]["id"]; +function settingsTabId(id: SectionId) { + return `settings-tab-${id}`; +} + +function settingsPanelId(id: SectionId) { + return `settings-panel-${id}`; +} + // ─── Copy-to-clipboard helper ───────────────────────────────────────────────── function CopyButton({ value }: { value: string }) { @@ -102,15 +110,17 @@ function Toggle({ @@ -220,10 +230,11 @@ function WalletSection() {

{network?.isSupported ? "Supported" : "Unsupported"} @@ -401,9 +412,7 @@ function NotificationsSection() { } /> {phoneError && ( -

- {phoneError} -

+

{phoneError}

)} @@ -499,10 +508,11 @@ function SecuritySection() {
KYC Status {user?.kycVerified ? "Verified" : "Not Verified"} @@ -592,10 +602,11 @@ function DisplaySection() { @@ -634,6 +645,32 @@ export default function SettingsPage() { const [activeSection, setActiveSection] = useState("profile"); const handleLogout = () => logoutUser("manual"); + const activateSection = (id: SectionId) => { + setActiveSection(id); + requestAnimationFrame(() => { + document.getElementById(settingsTabId(id))?.focus(); + }); + }; + + const handleTabKeyDown = (event: KeyboardEvent, index: number) => { + let nextIndex: number | null = null; + + if (event.key === "ArrowRight" || event.key === "ArrowDown") { + nextIndex = (index + 1) % SECTIONS.length; + } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { + nextIndex = (index - 1 + SECTIONS.length) % SECTIONS.length; + } else if (event.key === "Home") { + nextIndex = 0; + } else if (event.key === "End") { + nextIndex = SECTIONS.length - 1; + } + + if (nextIndex === null) return; + + event.preventDefault(); + activateSection(SECTIONS[nextIndex].id); + }; + const renderSection = () => { switch (activeSection) { case "profile": @@ -674,26 +711,50 @@ export default function SettingsPage() {
{/* Side nav */} {/* Content */} -
{renderSection()}
+
+ {renderSection()} +
);