Add accessibility improvements (P4.2 partial)
All checks were successful
Deploy / deploy (push) Successful in 1m36s
All checks were successful
Deploy / deploy (push) Successful in 1m36s
- Add skip navigation link to root layout - Add semantic HTML landmarks (main element) to login and settings pages - Add aria-labels to calendar day buttons with date, cycle day, and phase info - Add id="main-content" to dashboard main element for skip link target - Fix pre-existing type error in auth-middleware.test.ts Tests: 781 passing (11 new accessibility tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
60
src/app/layout.test.tsx
Normal file
60
src/app/layout.test.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// ABOUTME: Unit tests for the root layout component.
|
||||
// ABOUTME: Tests accessibility features including skip navigation link.
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock next/font/google before importing the layout
|
||||
vi.mock("next/font/google", () => ({
|
||||
Geist: () => ({
|
||||
variable: "--font-geist-sans",
|
||||
className: "geist-sans",
|
||||
}),
|
||||
Geist_Mono: () => ({
|
||||
variable: "--font-geist-mono",
|
||||
className: "geist-mono",
|
||||
}),
|
||||
}));
|
||||
|
||||
import RootLayout from "./layout";
|
||||
|
||||
describe("RootLayout", () => {
|
||||
describe("accessibility", () => {
|
||||
it("renders a skip navigation link as the first focusable element", () => {
|
||||
render(
|
||||
<RootLayout>
|
||||
<main id="main-content">Test content</main>
|
||||
</RootLayout>,
|
||||
);
|
||||
|
||||
const skipLink = screen.getByRole("link", {
|
||||
name: /skip to main content/i,
|
||||
});
|
||||
expect(skipLink).toBeInTheDocument();
|
||||
expect(skipLink).toHaveAttribute("href", "#main-content");
|
||||
});
|
||||
|
||||
it("skip link has visually hidden styles but is focusable", () => {
|
||||
render(
|
||||
<RootLayout>
|
||||
<main id="main-content">Test content</main>
|
||||
</RootLayout>,
|
||||
);
|
||||
|
||||
const skipLink = screen.getByRole("link", {
|
||||
name: /skip to main content/i,
|
||||
});
|
||||
expect(skipLink).toHaveClass("sr-only");
|
||||
expect(skipLink).toHaveClass("focus:not-sr-only");
|
||||
});
|
||||
|
||||
it("renders children within the body", () => {
|
||||
render(
|
||||
<RootLayout>
|
||||
<div data-testid="child-content">Test content</div>
|
||||
</RootLayout>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,12 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:rounded focus:shadow-lg focus:text-blue-600 focus:underline"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -541,4 +541,24 @@ describe("LoginPage", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("accessibility", () => {
|
||||
it("wraps content in a main element", async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("main")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper heading structure with h1", async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
const heading = screen.getByRole("heading", { level: 1 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(/phaseflow/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,17 +97,23 @@ export default function LoginPage() {
|
||||
// Show loading state while checking auth methods
|
||||
if (isCheckingAuth) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<main
|
||||
id="main-content"
|
||||
className="flex min-h-screen items-center justify-center"
|
||||
>
|
||||
<div className="w-full max-w-md space-y-8 p-8">
|
||||
<h1 className="text-2xl font-bold text-center">PhaseFlow</h1>
|
||||
<div className="text-center text-gray-500">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<main
|
||||
id="main-content"
|
||||
className="flex min-h-screen items-center justify-center"
|
||||
>
|
||||
<div className="w-full max-w-md space-y-8 p-8">
|
||||
<h1 className="text-2xl font-bold text-center">PhaseFlow</h1>
|
||||
|
||||
@@ -181,6 +187,6 @@ export default function LoginPage() {
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto p-6">
|
||||
<main id="main-content" className="container mx-auto p-6">
|
||||
{loading && <DashboardSkeleton />}
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -505,4 +505,24 @@ describe("SettingsPage", () => {
|
||||
expect(screen.queryByText(/settings saved/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("accessibility", () => {
|
||||
it("wraps content in a main element", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("main")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("has proper heading structure with h1", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
const heading = screen.getByRole("heading", { level: 1 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(/settings/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,15 +104,15 @@ export default function SettingsPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<main id="main-content" className="container mx-auto p-8">
|
||||
<h1 className="text-2xl font-bold mb-8">Settings</h1>
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<main id="main-content" className="container mx-auto p-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold">Settings</h1>
|
||||
<Link
|
||||
@@ -247,6 +247,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user