diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 6769a87..ab17d1d 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -1,5 +1,7 @@ import { PageShell } from "@/components/PageShell"; +import { CurlBlock } from "@/components/CurlBlock"; import { messages } from "@/lib/messages"; +import { resolveApiBase } from "@/lib/resolveApiBase"; export const metadata = { title: "Docs — AgentPay" }; diff --git a/src/app/settings/page.test.tsx b/src/app/settings/page.test.tsx new file mode 100644 index 0000000..fe7fa35 --- /dev/null +++ b/src/app/settings/page.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from "@testing-library/react"; +import SettingsPage from "./page"; +import { messages } from "@/lib/messages"; + +/** + * jsdom does not implement `window.matchMedia`, which `ThemeToggle` reads via + * `readTheme()` / `effectiveTheme()`. Stub it so the page renders without + * throwing. Mirrors the stub used in the ThemeToggle component test. + */ +const mockMatchMedia = (matches: boolean) => { + Object.defineProperty(window, "matchMedia", { + configurable: true, + writable: true, + value: jest.fn().mockImplementation((query: string) => ({ + matches, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); +}; + +describe("SettingsPage", () => { + beforeEach(() => { + window.localStorage.clear(); + document.documentElement.classList.remove("dark"); + mockMatchMedia(false); + }); + + it("renders the Settings page heading", () => { + render(); + expect( + screen.getByRole("heading", { level: 1, name: messages.settings.heading }), + ).toBeInTheDocument(); + // Sanity-check the literal copy so a messages refactor cannot silently + // change the visible heading. + expect( + screen.getByRole("heading", { level: 1, name: "Settings" }), + ).toBeInTheDocument(); + }); + + it("renders the Appearance section heading and descriptive copy", () => { + render(); + expect( + screen.getByRole("heading", { + level: 2, + name: messages.settings.appearance.heading, + }), + ).toBeInTheDocument(); + expect( + screen.getByText(messages.settings.appearance.description), + ).toBeInTheDocument(); + }); + + it("renders the ThemeToggle control", () => { + render(); + // ThemeToggle exposes a labelled radio-like button group. + expect( + screen.getByRole("group", { name: "Theme" }), + ).toBeInTheDocument(); + for (const option of ["light", "dark", "system"]) { + expect( + screen.getByRole("button", { name: option }), + ).toBeInTheDocument(); + } + }); + + it("exposes the main landmark with id='main-content' for the skip link", () => { + render(); + const main = screen.getByRole("main"); + expect(main).toHaveAttribute("id", "main-content"); + }); + + it("renders without throwing when matchMedia is unavailable-shaped (matches=false)", () => { + // Guards the regression the issue calls out: the page must render in jsdom + // (no real matchMedia) once the stub is in place. + expect(() => render()).not.toThrow(); + }); +}); diff --git a/src/components/__tests__/Header.test.tsx b/src/components/__tests__/Header.test.tsx index 54d7962..faee338 100644 --- a/src/components/__tests__/Header.test.tsx +++ b/src/components/__tests__/Header.test.tsx @@ -53,14 +53,13 @@ describe("Header", () => { mockPathname.mockReturnValue("/"); render(
); - // Exactly one link has aria-current="page" + // Exactly one link has aria-current="page". The mobile menu panel is only + // rendered while open, so at rest only the desktop "Home" link is active. const activeLinks = screen.getAllByRole("link").filter( (link) => link.getAttribute("aria-current") === "page" ); - // Home link is active twice (desktop + mobile) - expect(activeLinks.length).toBe(2); + expect(activeLinks.length).toBe(1); expect(activeLinks[0]).toHaveTextContent("Home"); - expect(activeLinks[1]).toHaveTextContent("Home"); }); it("marks zero links as active for an unknown route", () => { @@ -80,14 +79,15 @@ describe("Header", () => { // Open the secondary menu to expose secondary links fireEvent.click(screen.getByRole("button", { name: /more/i })); - // The active links should strictly be the desktop "Services" and the mobile "Services" + // With the mobile panel closed, the only current link is the desktop + // "Services" primary link; the opened "More" menu holds secondary links, + // none of which match /services. const activeLinks = screen.getAllByRole("link", { hidden: true }).filter( (link) => link.getAttribute("aria-current") === "page" ); - - expect(activeLinks.length).toBe(2); + + expect(activeLinks.length).toBe(1); expect(activeLinks[0]).toHaveTextContent("Services"); - expect(activeLinks[1]).toHaveTextContent("Services"); }); it("shows More button that opens secondary menu", () => {