diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts
index 1e5482e..b138951 100644
--- a/e2e/smoke.spec.ts
+++ b/e2e/smoke.spec.ts
@@ -4,18 +4,23 @@ import * as fx from "../src/test/fixtures";
/**
* One smoke path: /login → submit → /home renders the real dashboard.
*
- * There's no live Go backend in CI, so this intercepts the network at the
- * browser layer (page.route) rather than standing up a stub server — the
- * Next dev server (and all of its own client/server code, including the
- * httpOnly-cookie auth route and the middleware guard) runs for real; only
- * the two things that would otherwise reach the Go backend
- * (`/api/auth/login`, `/api/v1/*`) are fulfilled with fixtures.
- *
- * Also asserts the two security-relevant properties the review flagged:
- * - the session cookie set on login is HttpOnly
- * - no response body observed during the flow contains a raw bearer token
- * (the Next auth route is supposed to strip `token` before it reaches the
- * browser — see src/app/api/auth/login/route.ts)
+ * There's no live Go backend in CI, so `page.route` intercepts
+ * `/api/auth/login` and `/api/v1/*` at the BROWSER network layer, before
+ * either request ever reaches the Next server — the real
+ * `src/app/api/auth/login/route.ts` handler (which calls the Go backend,
+ * strips `token`, and calls `buildSetCookie`) never runs in this test; this
+ * spec fabricates the `Set-Cookie` header and the token-free `{user}` body
+ * itself. What DOES run for real: the Next dev server's page rendering, the
+ * client-side login form + navigation, and — critically — the
+ * `src/middleware.ts` route guard, which reads the `mercury_session` cookie
+ * this test's fake `Set-Cookie` puts in the browser's real cookie jar to
+ * decide whether `/home` is reachable. So this smoke covers the
+ * login → navigate → guarded-route → dashboard-render path end to end; it
+ * does NOT cover the real auth route's token-stripping/cookie-building logic
+ * (that's covered separately by src/app/api/auth/auth.route.test.ts). The
+ * httpOnly/no-raw-token assertions below validate this test's own mocked
+ * response shape, i.e. they document the contract the real route must also
+ * satisfy — they are not independent proof that the real route satisfies it.
*/
test("login → home renders the dashboard; session cookie is httpOnly; no raw token leaks", async ({
page,
diff --git a/src/features/profile/SettingsForm.test.tsx b/src/features/profile/SettingsForm.test.tsx
index 15990a8..70900b7 100644
--- a/src/features/profile/SettingsForm.test.tsx
+++ b/src/features/profile/SettingsForm.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen } from "@testing-library/react";
+import { render, screen, fireEvent } from "@testing-library/react";
import { it, expect, vi } from "vitest";
import type { Settings } from "@/api/schemas";
@@ -31,3 +31,16 @@ it("renders the holder-name field pre-filled from useSettings", () => {
const input = screen.getByLabelText("Данс эзэмшигчийн нэр") as HTMLInputElement;
expect(input.value).toBe(settingsFixture.holderName);
});
+
+// Flagged in review: SwitchRoot needs its SwitchHiddenInput sibling to be an
+// accessible, interactive `role="switch"` at all — SwitchControl/SwitchThumb
+// are aria-hidden decoration only (same bug fixed in ds/HideAmountsToggle.tsx).
+it("hideAmounts switch is an accessible, toggleable role=switch bound to form state", () => {
+ render();
+ const toggle = screen.getByRole("switch", { name: "Үнийн дүн нуух" });
+ expect(toggle).not.toBeChecked();
+
+ fireEvent.click(toggle);
+
+ expect(toggle).toBeChecked();
+});
diff --git a/src/features/profile/SettingsForm.tsx b/src/features/profile/SettingsForm.tsx
index a3abb80..e9a315e 100644
--- a/src/features/profile/SettingsForm.tsx
+++ b/src/features/profile/SettingsForm.tsx
@@ -1,7 +1,16 @@
"use client";
import * as React from "react";
-import { TextFieldRoot, TextFieldInput, SwitchRoot, SwitchControl, SwitchThumb, SwitchLabel, Skeleton } from "@seed-design/react";
+import {
+ TextFieldRoot,
+ TextFieldInput,
+ SwitchRoot,
+ SwitchControl,
+ SwitchThumb,
+ SwitchLabel,
+ SwitchHiddenInput,
+ Skeleton,
+} from "@seed-design/react";
import { MercuryButton } from "@/ds/MercuryButton";
import { useSettings } from "@/api/hooks/reads";
import { useSaveSettings } from "@/api/hooks/mutations";
@@ -166,6 +175,12 @@ export function SettingsForm({ onBack }: SettingsFormProps) {
checked={form.hideAmounts}
onCheckedChange={(v: boolean) => setForm((f) => ({ ...f, hideAmounts: v }))}
>
+ {/* The actual interactive/accessible element (role="switch",
+ checked/onChange) lives on the hidden input — SwitchControl and
+ SwitchThumb are purely decorative (aria-hidden). Without this
+ the switch renders but nothing is clickable or announced to
+ assistive tech (same bug as ds/HideAmountsToggle.tsx). */}
+