Sergei 1628501e75 Add Layout components for web application
Implemented responsive layout system with:
- Header: Top navigation with profile menu and mobile hamburger
- Sidebar: Desktop-only navigation sidebar (lg and above)
- Breadcrumbs: Auto-generated navigation breadcrumbs
- Layout: Main wrapper component with configurable options

Features:
- Responsive design (mobile, tablet, desktop)
- Active route highlighting
- User profile integration via auth store
- Click-outside dropdown closing
- Comprehensive test coverage (49 tests passing)

Updated (main) layout to use new Layout component system.
Updated dashboard page to work with new layout structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 18:20:13 -08:00

222 lines
5.4 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import { Layout } from '../Layout';
import { useAuthStore } from '@/stores/authStore';
import { usePathname } from 'next/navigation';
// Mock child components
jest.mock('../Header', () => ({
Header: () => <div data-testid="header">Header</div>,
}));
jest.mock('../Sidebar', () => ({
Sidebar: () => <div data-testid="sidebar">Sidebar</div>,
}));
jest.mock('../Breadcrumbs', () => ({
Breadcrumbs: () => <div data-testid="breadcrumbs">Breadcrumbs</div>,
}));
// Mock Next.js navigation
jest.mock('next/navigation', () => ({
usePathname: jest.fn(),
}));
// Mock auth store
jest.mock('@/stores/authStore', () => ({
useAuthStore: jest.fn(),
}));
describe('Layout Component', () => {
beforeEach(() => {
jest.clearAllMocks();
(usePathname as jest.Mock).mockReturnValue('/dashboard');
(useAuthStore as unknown as jest.Mock).mockReturnValue({
user: { user_id: 1, email: 'test@example.com' },
logout: jest.fn(),
});
});
it('renders children content', () => {
render(
<Layout>
<div>Test Content</div>
</Layout>
);
expect(screen.getByText('Test Content')).toBeInTheDocument();
});
it('renders header by default', () => {
render(
<Layout>
<div>Content</div>
</Layout>
);
expect(screen.getByTestId('header')).toBeInTheDocument();
});
it('renders sidebar by default', () => {
render(
<Layout>
<div>Content</div>
</Layout>
);
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
});
it('renders breadcrumbs by default', () => {
render(
<Layout>
<div>Content</div>
</Layout>
);
expect(screen.getByTestId('breadcrumbs')).toBeInTheDocument();
});
it('hides header when showHeader is false', () => {
render(
<Layout showHeader={false}>
<div>Content</div>
</Layout>
);
expect(screen.queryByTestId('header')).not.toBeInTheDocument();
});
it('hides sidebar when showSidebar is false', () => {
render(
<Layout showSidebar={false}>
<div>Content</div>
</Layout>
);
expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument();
});
it('hides breadcrumbs when showBreadcrumbs is false', () => {
render(
<Layout showBreadcrumbs={false}>
<div>Content</div>
</Layout>
);
expect(screen.queryByTestId('breadcrumbs')).not.toBeInTheDocument();
});
it('renders page title when provided', () => {
render(
<Layout title="Dashboard">
<div>Content</div>
</Layout>
);
expect(screen.getByText('Dashboard')).toBeInTheDocument();
});
it('applies correct max-width class (7xl by default)', () => {
const { container } = render(
<Layout>
<div>Content</div>
</Layout>
);
const main = container.querySelector('main div');
expect(main).toHaveClass('max-w-7xl');
});
it('applies custom max-width class', () => {
const { container } = render(
<Layout maxWidth="4xl">
<div>Content</div>
</Layout>
);
const main = container.querySelector('main div');
expect(main).toHaveClass('max-w-4xl');
});
it('applies full-width when maxWidth is full', () => {
const { container } = render(
<Layout maxWidth="full">
<div>Content</div>
</Layout>
);
const main = container.querySelector('main div');
expect(main).not.toHaveClass('max-w-7xl');
expect(main).not.toHaveClass('max-w-4xl');
});
it('applies sidebar padding class to main area', () => {
const { container } = render(
<Layout showSidebar={true}>
<div>Content</div>
</Layout>
);
const mainWrapper = container.querySelector('.lg\\:pl-64');
expect(mainWrapper).toBeInTheDocument();
});
it('does not apply sidebar padding when sidebar is hidden', () => {
const { container } = render(
<Layout showSidebar={false}>
<div>Content</div>
</Layout>
);
const mainWrapper = container.querySelector('.lg\\:pl-64');
expect(mainWrapper).not.toBeInTheDocument();
});
it('has proper background color', () => {
const { container } = render(
<Layout>
<div>Content</div>
</Layout>
);
const wrapper = container.firstChild;
expect(wrapper).toHaveClass('bg-slate-50');
});
it('renders with all features enabled', () => {
render(
<Layout
showHeader={true}
showSidebar={true}
showBreadcrumbs={true}
title="Test Page"
>
<div>Content</div>
</Layout>
);
expect(screen.getByTestId('header')).toBeInTheDocument();
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
expect(screen.getByTestId('breadcrumbs')).toBeInTheDocument();
expect(screen.getByText('Test Page')).toBeInTheDocument();
expect(screen.getByText('Content')).toBeInTheDocument();
});
it('renders minimal layout without any chrome', () => {
render(
<Layout
showHeader={false}
showSidebar={false}
showBreadcrumbs={false}
>
<div>Content</div>
</Layout>
);
expect(screen.queryByTestId('header')).not.toBeInTheDocument();
expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument();
expect(screen.queryByTestId('breadcrumbs')).not.toBeInTheDocument();
expect(screen.getByText('Content')).toBeInTheDocument();
});
});