Implements the main navigation and layout structure: - Sidebar component with collapsible navigation and keyboard shortcut - AppHeader with project switcher and user menu - AppBreadcrumbs with auto-generation from pathname - ProjectSwitcher dropdown for quick project navigation - UserMenu with profile, settings, and logout - AppLayout component combining all layout elements Features: - Responsive design (mobile sidebar sheet, desktop sidebar) - Keyboard navigation (Cmd/Ctrl+B to toggle sidebar) - Dark mode support - WCAG AA accessible (ARIA labels, focus management) All 125 tests passing. Follows design system guidelines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
/**
|
|
* Tests for AppBreadcrumbs Component
|
|
* Verifies breadcrumb generation, navigation, and accessibility
|
|
*/
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import { AppBreadcrumbs } from '@/components/layout/AppBreadcrumbs';
|
|
import { mockUsePathname } from 'next-intl/navigation';
|
|
|
|
describe('AppBreadcrumbs', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockUsePathname.mockReturnValue('/projects');
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('renders breadcrumb navigation', () => {
|
|
render(<AppBreadcrumbs />);
|
|
expect(screen.getByTestId('breadcrumbs')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders with correct aria-label', () => {
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const nav = screen.getByTestId('breadcrumbs');
|
|
expect(nav).toHaveAttribute('aria-label', 'Breadcrumb');
|
|
});
|
|
|
|
it('renders home icon by default', () => {
|
|
render(<AppBreadcrumbs />);
|
|
expect(screen.getByTestId('breadcrumb-home')).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides home icon when showHome is false', () => {
|
|
render(<AppBreadcrumbs showHome={false} />);
|
|
expect(screen.queryByTestId('breadcrumb-home')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Auto-generated Breadcrumbs', () => {
|
|
it('generates breadcrumb from pathname', () => {
|
|
mockUsePathname.mockReturnValue('/projects');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
expect(screen.getByTestId('breadcrumb-projects')).toBeInTheDocument();
|
|
});
|
|
|
|
it('generates nested breadcrumbs', () => {
|
|
mockUsePathname.mockReturnValue('/projects/my-project/issues');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
expect(screen.getByTestId('breadcrumb-projects')).toBeInTheDocument();
|
|
expect(screen.getByTestId('breadcrumb-my-project')).toBeInTheDocument();
|
|
expect(screen.getByTestId('breadcrumb-issues')).toBeInTheDocument();
|
|
});
|
|
|
|
it('uses label mappings for known paths', () => {
|
|
mockUsePathname.mockReturnValue('/admin/agent-types');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
expect(screen.getByText('Admin')).toBeInTheDocument();
|
|
expect(screen.getByText('Agent Types')).toBeInTheDocument();
|
|
});
|
|
|
|
it('uses segment as label for unknown paths', () => {
|
|
mockUsePathname.mockReturnValue('/custom-path');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
expect(screen.getByText('custom-path')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Custom Breadcrumbs', () => {
|
|
it('uses provided items instead of auto-generation', () => {
|
|
mockUsePathname.mockReturnValue('/some/path');
|
|
|
|
const items = [
|
|
{ label: 'Custom', href: '/custom', current: false },
|
|
{ label: 'Path', href: '/custom/path', current: true },
|
|
];
|
|
|
|
render(<AppBreadcrumbs items={items} />);
|
|
|
|
expect(screen.getByTestId('breadcrumb-custom')).toBeInTheDocument();
|
|
expect(screen.getByTestId('breadcrumb-path')).toBeInTheDocument();
|
|
expect(screen.queryByText('some')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Active State', () => {
|
|
it('marks last item as current page', () => {
|
|
mockUsePathname.mockReturnValue('/projects/issues');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const issuesItem = screen.getByTestId('breadcrumb-issues');
|
|
expect(issuesItem).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
it('renders non-current items as links', () => {
|
|
mockUsePathname.mockReturnValue('/projects/issues');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const projectsLink = screen.getByTestId('breadcrumb-projects');
|
|
expect(projectsLink.tagName).toBe('A');
|
|
expect(projectsLink).toHaveAttribute('href', '/projects');
|
|
});
|
|
|
|
it('renders current item as span (not a link)', () => {
|
|
mockUsePathname.mockReturnValue('/projects');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const projectsItem = screen.getByTestId('breadcrumb-projects');
|
|
expect(projectsItem.tagName).toBe('SPAN');
|
|
});
|
|
});
|
|
|
|
describe('Separators', () => {
|
|
it('renders chevron separators between items', () => {
|
|
mockUsePathname.mockReturnValue('/projects/issues');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
// There should be separators: home -> projects -> issues
|
|
const separators = screen.getAllByRole('listitem');
|
|
expect(separators.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
describe('Empty State', () => {
|
|
it('returns null when no breadcrumbs', () => {
|
|
mockUsePathname.mockReturnValue('/');
|
|
|
|
const { container } = render(<AppBreadcrumbs showHome={false} />);
|
|
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('home link has accessible label', () => {
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const homeLink = screen.getByTestId('breadcrumb-home');
|
|
expect(homeLink).toHaveAttribute('aria-label', 'Home');
|
|
});
|
|
|
|
it('links have focus-visible styling', () => {
|
|
mockUsePathname.mockReturnValue('/projects/issues');
|
|
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const projectsLink = screen.getByTestId('breadcrumb-projects');
|
|
expect(projectsLink).toHaveClass('focus-visible:ring-2');
|
|
});
|
|
|
|
it('navigation has proper landmark role', () => {
|
|
render(<AppBreadcrumbs />);
|
|
|
|
const nav = screen.getByRole('navigation');
|
|
expect(nav).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Custom className', () => {
|
|
it('applies custom className', () => {
|
|
render(<AppBreadcrumbs className="custom-class" />);
|
|
|
|
const nav = screen.getByTestId('breadcrumbs');
|
|
expect(nav).toHaveClass('custom-class');
|
|
});
|
|
});
|
|
});
|