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>
156 lines
4.5 KiB
TypeScript
156 lines
4.5 KiB
TypeScript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { Header } from '../Header';
|
|
import { useAuthStore } from '@/stores/authStore';
|
|
import { usePathname } from 'next/navigation';
|
|
|
|
// Mock Next.js navigation
|
|
jest.mock('next/navigation', () => ({
|
|
usePathname: jest.fn(),
|
|
}));
|
|
|
|
// Mock auth store
|
|
jest.mock('@/stores/authStore', () => ({
|
|
useAuthStore: jest.fn(),
|
|
}));
|
|
|
|
describe('Header Component', () => {
|
|
const mockLogout = jest.fn();
|
|
const mockUser = {
|
|
user_id: 1,
|
|
email: 'test@example.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
(usePathname as jest.Mock).mockReturnValue('/dashboard');
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
user: mockUser,
|
|
logout: mockLogout,
|
|
});
|
|
});
|
|
|
|
it('renders the WellNuo logo and brand', () => {
|
|
render(<Header />);
|
|
expect(screen.getByText('WellNuo')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders navigation items', () => {
|
|
render(<Header />);
|
|
expect(screen.getAllByText('Dashboard')).toHaveLength(1);
|
|
expect(screen.getAllByText('Loved Ones')).toHaveLength(1);
|
|
});
|
|
|
|
it('highlights active navigation item', () => {
|
|
render(<Header />);
|
|
const dashboardLink = screen.getAllByText('Dashboard')[0].closest('a');
|
|
expect(dashboardLink).toHaveClass('text-blue-600');
|
|
});
|
|
|
|
it('displays user initials in profile button', () => {
|
|
render(<Header />);
|
|
// Desktop profile button shows initials
|
|
expect(screen.getByText('JD')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays user full name on desktop', () => {
|
|
render(<Header />);
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens profile dropdown when clicked', async () => {
|
|
render(<Header />);
|
|
|
|
// Get profile button (not the mobile menu button)
|
|
const buttons = screen.getAllByRole('button', { expanded: false });
|
|
const profileButton = buttons.find(btn => btn.getAttribute('aria-haspopup') === 'true');
|
|
|
|
if (!profileButton) throw new Error('Profile button not found');
|
|
|
|
fireEvent.click(profileButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Profile Settings')).toBeInTheDocument();
|
|
expect(screen.getByText('Sign Out')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls logout when Sign Out is clicked in dropdown', async () => {
|
|
render(<Header />);
|
|
|
|
// Open dropdown
|
|
const buttons = screen.getAllByRole('button', { expanded: false });
|
|
const profileButton = buttons.find(btn => btn.getAttribute('aria-haspopup') === 'true');
|
|
if (!profileButton) throw new Error('Profile button not found');
|
|
|
|
fireEvent.click(profileButton);
|
|
|
|
// Click Sign Out in dropdown (not mobile menu)
|
|
await waitFor(() => {
|
|
const signOutButtons = screen.getAllByRole('button', { name: /sign out/i });
|
|
fireEvent.click(signOutButtons[0]);
|
|
});
|
|
|
|
expect(mockLogout).toHaveBeenCalled();
|
|
});
|
|
|
|
it('toggles mobile menu', async () => {
|
|
render(<Header />);
|
|
|
|
const menuButton = screen.getByLabelText('Toggle menu');
|
|
fireEvent.click(menuButton);
|
|
|
|
await waitFor(() => {
|
|
// Mobile menu should show navigation items
|
|
expect(screen.getByText('Profile Settings')).toBeInTheDocument();
|
|
});
|
|
|
|
// Close menu
|
|
fireEvent.click(menuButton);
|
|
});
|
|
|
|
it('displays user email when no first/last name', () => {
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
user: { user_id: 1, email: 'test@example.com' },
|
|
logout: mockLogout,
|
|
});
|
|
|
|
render(<Header />);
|
|
// Email displayed in profile section
|
|
expect(screen.getByText('test@example.com')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays fallback initials when no user data', () => {
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
user: { user_id: 1 },
|
|
logout: mockLogout,
|
|
});
|
|
|
|
render(<Header />);
|
|
expect(screen.getByText('?')).toBeInTheDocument();
|
|
});
|
|
|
|
it('closes dropdown when clicking outside', async () => {
|
|
render(<Header />);
|
|
|
|
// Open dropdown
|
|
const buttons = screen.getAllByRole('button', { expanded: false });
|
|
const profileButton = buttons.find(btn => btn.getAttribute('aria-haspopup') === 'true');
|
|
if (!profileButton) throw new Error('Profile button not found');
|
|
|
|
fireEvent.click(profileButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Profile Settings')).toBeInTheDocument();
|
|
});
|
|
|
|
// Click outside
|
|
fireEvent.mouseDown(document.body);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Profile Settings')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|