/** * 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(); expect(screen.getByTestId('create-project-button')).toBeInTheDocument(); expect(screen.getByText('Create Project')).toBeInTheDocument(); }); it('renders project switcher trigger when projects exist', () => { render(); expect(screen.getByTestId('project-switcher-trigger')).toBeInTheDocument(); }); it('displays current project name', () => { render( ); expect(screen.getByText('Project One')).toBeInTheDocument(); }); it('displays placeholder when no current project', () => { render(); expect(screen.getByText('Select Project')).toBeInTheDocument(); }); }); describe('Dropdown Menu', () => { it('opens dropdown when trigger is clicked', async () => { const user = userEvent.setup(); render(); 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(); 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( ); 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(); 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(); 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( ); 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(); 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(); 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( ); 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(); 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( ); expect(screen.getByTestId('project-select')).toBeInTheDocument(); }); it('displays placeholder', () => { render( ); expect(screen.getByText('Choose a project')).toBeInTheDocument(); }); it('has combobox role', () => { render( ); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('applies custom className', () => { render( ); 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. });