fix(web): SwitchHiddenInput on settings hideAmounts toggle + correct smoke comment

This commit is contained in:
Munkherdene 2026-08-22 21:16:45 +08:00
parent 6e84d99d46
commit b02674aa62
3 changed files with 47 additions and 14 deletions

View file

@ -4,18 +4,23 @@ import * as fx from "../src/test/fixtures";
/** /**
* One smoke path: /login submit /home renders the real dashboard. * 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 * There's no live Go backend in CI, so `page.route` intercepts
* browser layer (page.route) rather than standing up a stub server the * `/api/auth/login` and `/api/v1/*` at the BROWSER network layer, before
* Next dev server (and all of its own client/server code, including the * either request ever reaches the Next server the real
* httpOnly-cookie auth route and the middleware guard) runs for real; only * `src/app/api/auth/login/route.ts` handler (which calls the Go backend,
* the two things that would otherwise reach the Go backend * strips `token`, and calls `buildSetCookie`) never runs in this test; this
* (`/api/auth/login`, `/api/v1/*`) are fulfilled with fixtures. * 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
* Also asserts the two security-relevant properties the review flagged: * client-side login form + navigation, and critically the
* - the session cookie set on login is HttpOnly * `src/middleware.ts` route guard, which reads the `mercury_session` cookie
* - no response body observed during the flow contains a raw bearer token * this test's fake `Set-Cookie` puts in the browser's real cookie jar to
* (the Next auth route is supposed to strip `token` before it reaches the * decide whether `/home` is reachable. So this smoke covers the
* browser see src/app/api/auth/login/route.ts) * 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 ({ test("login → home renders the dashboard; session cookie is httpOnly; no raw token leaks", async ({
page, page,

View file

@ -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 { it, expect, vi } from "vitest";
import type { Settings } from "@/api/schemas"; 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; const input = screen.getByLabelText("Данс эзэмшигчийн нэр") as HTMLInputElement;
expect(input.value).toBe(settingsFixture.holderName); 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(<SettingsForm />);
const toggle = screen.getByRole("switch", { name: "Үнийн дүн нуух" });
expect(toggle).not.toBeChecked();
fireEvent.click(toggle);
expect(toggle).toBeChecked();
});

View file

@ -1,7 +1,16 @@
"use client"; "use client";
import * as React from "react"; 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 { MercuryButton } from "@/ds/MercuryButton";
import { useSettings } from "@/api/hooks/reads"; import { useSettings } from "@/api/hooks/reads";
import { useSaveSettings } from "@/api/hooks/mutations"; import { useSaveSettings } from "@/api/hooks/mutations";
@ -166,6 +175,12 @@ export function SettingsForm({ onBack }: SettingsFormProps) {
checked={form.hideAmounts} checked={form.hideAmounts}
onCheckedChange={(v: boolean) => setForm((f) => ({ ...f, hideAmounts: v }))} 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). */}
<SwitchHiddenInput />
<SwitchControl> <SwitchControl>
<SwitchThumb /> <SwitchThumb />
</SwitchControl> </SwitchControl>