diff --git a/frontend/src/components/AppLayout.test.tsx b/frontend/src/components/AppLayout.test.tsx
new file mode 100644
index 0000000..6b11261
--- /dev/null
+++ b/frontend/src/components/AppLayout.test.tsx
@@ -0,0 +1,95 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { AppLayout } from './AppLayout'
+import type { AuthContext } from '@/hooks/useAuth'
+
+// Mock ResizeObserver for sidebar tests
+globalThis.ResizeObserver = class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+} as any
+
+// Mock matchMedia for use-mobile hook used by SidebarProvider
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+})
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
+}))
+
+vi.mock('react-router-dom', () => ({
+ Link: ({ children, to, ...props }: any) => (
+
+ {children}
+
+ ),
+ useLocation: () => ({ pathname: '/' }),
+}))
+
+const mockAuth: AuthContext = {
+ user: { display_name: 'Test' } as any,
+ loading: false,
+ login: vi.fn(),
+ register: vi.fn(),
+ logout: vi.fn(),
+ token: 'test',
+ refetch: vi.fn(),
+} as unknown as AuthContext
+
+describe('AppLayout', () => {
+ it('NAV-01: sidebar element renders with distinct background', () => {
+ render(
+
+ content
+
+ )
+ // The sidebar renders with data-sidebar="sidebar" -- bg-sidebar token provides pastel background
+ const sidebar = document.querySelector('[data-sidebar="sidebar"]')
+ expect(sidebar).toBeInTheDocument()
+ })
+
+ it('NAV-02: gradient wordmark renders in sidebar header', () => {
+ render(
+
+ content
+
+ )
+ const wordmark = screen.getByTestId('sidebar-wordmark')
+ expect(wordmark).toBeInTheDocument()
+ })
+
+ it('NAV-03: active nav item (dashboard at /) has data-active="true"', () => {
+ render(
+
+ content
+
+ )
+ const dashboardLink = screen.getByRole('link', { name: /nav\.dashboard/i })
+ // SidebarMenuButton sets data-active on itself; it wraps the Link via asChild
+ const menuButton = dashboardLink.closest('[data-active]')
+ expect(menuButton).toHaveAttribute('data-active', 'true')
+ })
+
+ it('NAV-04: SidebarTrigger collapse button is rendered', () => {
+ render(
+
+ content
+
+ )
+ // SidebarTrigger renders with sr-only text "Toggle Sidebar"
+ const trigger = screen.getByRole('button', { name: /toggle sidebar/i })
+ expect(trigger).toBeInTheDocument()
+ })
+})