From 9b57a1a4bef69101a5916708330e0d17df1ef80b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 11 Mar 2026 21:48:15 +0100 Subject: [PATCH] test(02-02): add AppLayout test scaffold for NAV-01 through NAV-04 - Mock ResizeObserver and matchMedia for SidebarProvider compatibility - NAV-01: verify sidebar data-sidebar element renders - NAV-02: verify data-testid=sidebar-wordmark gradient element - NAV-03: verify active nav item has data-active=true - NAV-04: verify SidebarTrigger Toggle Sidebar button renders --- frontend/src/components/AppLayout.test.tsx | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 frontend/src/components/AppLayout.test.tsx 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() + }) +})