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", () => {