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). */} +