forked from cardosofelipe/fast-next-template
- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits. - Improved consistency in component imports by grouping and consolidating them. - No functional changes, purely restructuring for clarity and maintainability.
237 lines
7.5 KiB
TypeScript
237 lines
7.5 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.
|
|
});
|