Files
fast-next-template/frontend/tests/components/layout/ProjectSwitcher.test.tsx
Felipe Cardoso 6e645835dc feat(frontend): Implement navigation and layout (#44)
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>
2025-12-30 01:35:39 +01:00

274 lines
7.9 KiB
TypeScript

/**
* Tests for ProjectSwitcher Component
* Verifies project selection, navigation, and accessibility
*/
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ProjectSwitcher, ProjectSelect } from '@/components/layout/ProjectSwitcher';
import { mockUseRouter } from 'next-intl/navigation';
// Mock useRouter
const mockPush = jest.fn();
describe('ProjectSwitcher', () => {
const mockProjects = [
{ id: '1', slug: 'project-one', name: 'Project One' },
{ id: '2', slug: 'project-two', name: 'Project Two' },
{ id: '3', slug: 'project-three', name: 'Project Three' },
];
beforeEach(() => {
jest.clearAllMocks();
mockUseRouter.mockReturnValue({
push: mockPush,
replace: jest.fn(),
prefetch: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
refresh: jest.fn(),
});
});
describe('Rendering', () => {
it('renders create project button when no projects', () => {
render(<ProjectSwitcher projects={[]} />);
expect(screen.getByTestId('create-project-button')).toBeInTheDocument();
expect(screen.getByText('Create Project')).toBeInTheDocument();
});
it('renders project switcher trigger when projects exist', () => {
render(<ProjectSwitcher projects={mockProjects} />);
expect(screen.getByTestId('project-switcher-trigger')).toBeInTheDocument();
});
it('displays current project name', () => {
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
expect(screen.getByText('Project One')).toBeInTheDocument();
});
it('displays placeholder when no current project', () => {
render(<ProjectSwitcher projects={mockProjects} />);
expect(screen.getByText('Select Project')).toBeInTheDocument();
});
});
describe('Dropdown Menu', () => {
it('opens dropdown when trigger is clicked', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
await waitFor(() => {
expect(screen.getByTestId('project-switcher-menu')).toBeInTheDocument();
});
});
it('displays all projects in dropdown', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
await waitFor(() => {
expect(screen.getByTestId('project-option-project-one')).toBeInTheDocument();
expect(screen.getByTestId('project-option-project-two')).toBeInTheDocument();
expect(screen.getByTestId('project-option-project-three')).toBeInTheDocument();
});
});
it('shows current indicator on selected project', async () => {
const user = userEvent.setup();
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
await waitFor(() => {
const currentOption = screen.getByTestId('project-option-project-one');
expect(currentOption).toHaveTextContent('Current');
});
});
it('includes create new project option', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
await waitFor(() => {
expect(screen.getByTestId('create-project-option')).toBeInTheDocument();
expect(screen.getByText('Create New Project')).toBeInTheDocument();
});
});
});
describe('Navigation', () => {
it('navigates to project when option is selected', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
const projectOption = await screen.findByTestId('project-option-project-two');
await user.click(projectOption);
expect(mockPush).toHaveBeenCalledWith('/projects/project-two');
});
it('calls onProjectChange callback when provided', async () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();
render(
<ProjectSwitcher
projects={mockProjects}
onProjectChange={mockOnChange}
/>
);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
const projectOption = await screen.findByTestId('project-option-project-two');
await user.click(projectOption);
expect(mockOnChange).toHaveBeenCalledWith('project-two');
expect(mockPush).not.toHaveBeenCalled();
});
it('navigates to create project page', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
const createOption = await screen.findByTestId('create-project-option');
await user.click(createOption);
expect(mockPush).toHaveBeenCalledWith('/projects/new');
});
it('navigates from empty state button', async () => {
const user = userEvent.setup();
render(<ProjectSwitcher projects={[]} />);
const createButton = screen.getByTestId('create-project-button');
await user.click(createButton);
expect(mockPush).toHaveBeenCalledWith('/projects/new');
});
});
describe('Accessibility', () => {
it('has accessible label on trigger', () => {
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
const trigger = screen.getByTestId('project-switcher-trigger');
expect(trigger).toHaveAttribute(
'aria-label',
'Switch project, current: Project One'
);
});
it('has accessible label when no current project', () => {
render(<ProjectSwitcher projects={mockProjects} />);
const trigger = screen.getByTestId('project-switcher-trigger');
expect(trigger).toHaveAttribute('aria-label', 'Select project');
});
});
});
describe('ProjectSelect', () => {
const mockProjects = [
{ id: '1', slug: 'project-one', name: 'Project One' },
{ id: '2', slug: 'project-two', name: 'Project Two' },
];
describe('Rendering', () => {
it('renders select component', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
/>
);
expect(screen.getByTestId('project-select')).toBeInTheDocument();
});
it('displays placeholder', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
placeholder="Choose a project"
/>
);
expect(screen.getByText('Choose a project')).toBeInTheDocument();
});
it('has combobox role', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
/>
);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('applies custom className', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
className="custom-class"
/>
);
const select = screen.getByTestId('project-select');
expect(select).toHaveClass('custom-class');
});
});
// Note: Selection interaction tests are skipped because Radix UI Select
// doesn't properly open in JSDOM environment. The component is tested
// through E2E tests instead.
});