Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -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" };

Expand Down
83 changes: 83 additions & 0 deletions src/app/settings/page.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<SettingsPage />);
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(<SettingsPage />);
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(<SettingsPage />);
// 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(<SettingsPage />);
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(<SettingsPage />)).not.toThrow();
});
});
16 changes: 8 additions & 8 deletions src/components/__tests__/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,13 @@ describe("Header", () => {
mockPathname.mockReturnValue("/");
render(<Header />);

// 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", () => {
Expand All @@ -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", () => {
Expand Down
Loading