test(frontend): comprehensive test coverage improvements and bug fixes

- Raise coverage thresholds to 90% statements/lines/functions, 85% branches
- Add comprehensive tests for ProjectDashboard, ProjectWizard, and all wizard steps
- Add tests for issue management: IssueDetailPanel, BulkActions, IssueFilters
- Expand IssueTable tests with keyboard navigation, dropdown menu, edge cases
- Add useIssues hook tests covering all mutations and optimistic updates
- Expand eventStore tests with selector hooks and additional scenarios
- Expand useProjectEvents tests with error recovery, ping events, edge cases
- Add PriorityBadge, StatusBadge, SyncStatusIndicator fallback branch tests
- Add constants.test.ts for comprehensive constant validation

Bug fixes:
- Fix false positive rollback test to properly verify onMutate context setup
- Replace deprecated substr() with substring() in mock helpers
- Fix type errors: ProjectComplexity, ClientMode enum values
- Fix unused imports and variables across test files
- Fix @ts-expect-error directives and method override signatures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 19:53:41 +01:00
parent 7280b182bd
commit 5c35702caf
20 changed files with 4579 additions and 6 deletions

View File

@@ -0,0 +1,152 @@
/**
* AgentChatStep Component Tests
*
* Tests for the agent chat placeholder/preview step.
*/
import { render, screen } from '@testing-library/react';
import { AgentChatStep } from '@/components/projects/wizard/steps/AgentChatStep';
describe('AgentChatStep', () => {
describe('Rendering', () => {
it('renders the step title', () => {
render(<AgentChatStep />);
expect(screen.getByText('Requirements Discovery')).toBeInTheDocument();
});
it('renders the Coming in Phase 4 badge', () => {
render(<AgentChatStep />);
expect(screen.getByText('Coming in Phase 4')).toBeInTheDocument();
});
it('renders the description text', () => {
render(<AgentChatStep />);
expect(screen.getByText(/chat with our product owner agent/i)).toBeInTheDocument();
});
it('renders the Preview Only badge', () => {
render(<AgentChatStep />);
expect(screen.getByText('Preview Only')).toBeInTheDocument();
});
});
describe('Agent Info', () => {
it('displays Product Owner Agent title', () => {
render(<AgentChatStep />);
expect(screen.getByText('Product Owner Agent')).toBeInTheDocument();
});
it('displays agent description', () => {
render(<AgentChatStep />);
expect(screen.getByText('Requirements discovery and sprint planning')).toBeInTheDocument();
});
});
describe('Mock Chat Messages', () => {
it('renders the chat log area', () => {
render(<AgentChatStep />);
expect(screen.getByRole('log', { name: /chat preview messages/i })).toBeInTheDocument();
});
it('renders agent messages', () => {
render(<AgentChatStep />);
expect(screen.getByText(/i'm your product owner agent/i)).toBeInTheDocument();
expect(screen.getByText(/let me break this down into user stories/i)).toBeInTheDocument();
});
it('renders user messages', () => {
render(<AgentChatStep />);
expect(screen.getByText(/i want to build an e-commerce platform/i)).toBeInTheDocument();
});
it('displays message timestamps', () => {
render(<AgentChatStep />);
expect(screen.getByText('10:00 AM')).toBeInTheDocument();
expect(screen.getByText('10:02 AM')).toBeInTheDocument();
expect(screen.getByText('10:03 AM')).toBeInTheDocument();
});
});
describe('Chat Input', () => {
it('renders disabled chat input', () => {
render(<AgentChatStep />);
const input = screen.getByRole('textbox', { name: /chat input/i });
expect(input).toBeDisabled();
});
it('shows placeholder text', () => {
render(<AgentChatStep />);
expect(screen.getByPlaceholderText(/disabled in preview/i)).toBeInTheDocument();
});
it('renders disabled send button', () => {
render(<AgentChatStep />);
const sendButton = screen.getByRole('button', { name: /send message/i });
expect(sendButton).toBeDisabled();
});
it('shows preview disclaimer', () => {
render(<AgentChatStep />);
expect(screen.getByText(/this chat interface is a preview/i)).toBeInTheDocument();
});
});
describe('Full Version Preview Card', () => {
it('renders the preview card title', () => {
render(<AgentChatStep />);
expect(screen.getByText('What to Expect in the Full Version')).toBeInTheDocument();
});
it('lists expected features', () => {
render(<AgentChatStep />);
expect(screen.getByText(/interactive requirements gathering/i)).toBeInTheDocument();
expect(screen.getByText(/architecture spike/i)).toBeInTheDocument();
expect(screen.getByText(/collaborative backlog creation/i)).toBeInTheDocument();
expect(screen.getByText(/real-time refinement/i)).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('icons have aria-hidden attribute', () => {
render(<AgentChatStep />);
const hiddenIcons = document.querySelectorAll('[aria-hidden="true"]');
expect(hiddenIcons.length).toBeGreaterThan(0);
});
it('chat log has appropriate role', () => {
render(<AgentChatStep />);
expect(screen.getByRole('log')).toBeInTheDocument();
});
it('disabled input has accessible label', () => {
render(<AgentChatStep />);
expect(screen.getByRole('textbox', { name: /chat input/i })).toBeInTheDocument();
});
it('disabled button has accessible label', () => {
render(<AgentChatStep />);
expect(screen.getByRole('button', { name: /send message/i })).toBeInTheDocument();
});
});
describe('Visual elements', () => {
it('renders bot icons for agent messages', () => {
render(<AgentChatStep />);
const botIcons = document.querySelectorAll('.lucide-bot');
// Should have multiple bot icons (header + messages)
expect(botIcons.length).toBeGreaterThan(1);
});
it('renders user icon for user messages', () => {
render(<AgentChatStep />);
const userIcons = document.querySelectorAll('.lucide-user');
expect(userIcons.length).toBeGreaterThan(0);
});
it('renders sparkles icon for features preview', () => {
render(<AgentChatStep />);
const sparklesIcons = document.querySelectorAll('.lucide-sparkles');
expect(sparklesIcons.length).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,216 @@
/**
* AutonomyStep Component Tests
*
* Tests for the autonomy level selection step with approval matrix.
*/
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AutonomyStep } from '@/components/projects/wizard/steps/AutonomyStep';
import { autonomyOptions } from '@/components/projects/wizard/constants';
import { approvalLabels } from '@/components/projects/wizard/types';
import type { WizardState } from '@/components/projects/wizard/types';
describe('AutonomyStep', () => {
const defaultState: WizardState = {
step: 4,
projectName: 'Test Project',
description: '',
repoUrl: '',
complexity: 'medium',
clientMode: 'technical',
autonomyLevel: null,
};
const mockUpdateState = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders the step title', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText('Autonomy Level')).toBeInTheDocument();
});
it('renders the description text', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/how much control do you want/i)).toBeInTheDocument();
});
it('renders all autonomy options', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
autonomyOptions.forEach((option) => {
// Labels appear multiple times (in cards and matrix), use getAllByText
const labels = screen.getAllByText(option.label);
expect(labels.length).toBeGreaterThan(0);
expect(screen.getByText(option.description)).toBeInTheDocument();
});
});
it('renders "Best for" recommendations', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
autonomyOptions.forEach((option) => {
expect(screen.getByText(option.recommended)).toBeInTheDocument();
});
});
it('has accessible radiogroup role', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /autonomy level options/i })).toBeInTheDocument();
});
});
describe('Selection', () => {
it('calls updateState when clicking full_control option', async () => {
const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const fullControlOption = screen.getByRole('button', { name: /full control.*review every action/i });
await user.click(fullControlOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
});
it('calls updateState when clicking milestone option', async () => {
const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const milestoneOption = screen.getByRole('button', { name: /milestone.*review at sprint/i });
await user.click(milestoneOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'milestone' });
});
it('calls updateState when clicking autonomous option', async () => {
const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
});
it('shows visual selection indicator when an option is selected', () => {
const stateWithSelection: WizardState = {
...defaultState,
autonomyLevel: 'milestone',
};
render(<AutonomyStep state={stateWithSelection} updateState={mockUpdateState} />);
// The selected card should have the check icon
const checkIcons = document.querySelectorAll('.lucide-check');
expect(checkIcons.length).toBeGreaterThan(0);
});
});
describe('Approval Badges', () => {
it('renders approval badges for each option', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
// Check that badges are rendered with approval labels
Object.values(approvalLabels).forEach((label) => {
const badges = screen.getAllByText(new RegExp(label));
expect(badges.length).toBeGreaterThan(0);
});
});
it('shows Approve prefix for required approvals', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const approveBadges = screen.getAllByText(/^Approve:/);
expect(approveBadges.length).toBeGreaterThan(0);
});
it('shows Auto prefix for automatic approvals', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const autoBadges = screen.getAllByText(/^Auto:/);
expect(autoBadges.length).toBeGreaterThan(0);
});
});
describe('Approval Matrix Table', () => {
it('renders the approval matrix card', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText('Approval Matrix')).toBeInTheDocument();
});
it('renders table with column headers', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('columnheader', { name: 'Action Type' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: 'Full Control' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: 'Milestone' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: 'Autonomous' })).toBeInTheDocument();
});
it('renders all action types as rows', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const table = screen.getByRole('table', { name: /approval requirements/i });
Object.values(approvalLabels).forEach((label) => {
expect(within(table).getByText(label)).toBeInTheDocument();
});
});
it('shows Required badges in the matrix', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const requiredBadges = screen.getAllByText('Required');
expect(requiredBadges.length).toBeGreaterThan(0);
});
it('shows Automatic text in the matrix', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const automaticTexts = screen.getAllByText('Automatic');
expect(automaticTexts.length).toBeGreaterThan(0);
});
});
describe('Accessibility', () => {
it('each option has accessible aria-label', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
autonomyOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
});
expect(button).toBeInTheDocument();
});
});
it('table has accessible aria-label', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('table', { name: /approval requirements/i })).toBeInTheDocument();
});
it('icons have aria-hidden attribute', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const hiddenIcons = document.querySelectorAll('[aria-hidden="true"]');
expect(hiddenIcons.length).toBeGreaterThan(0);
});
});
describe('Edge cases', () => {
it('allows changing selection', async () => {
const user = userEvent.setup();
const stateWithFullControl: WizardState = {
...defaultState,
autonomyLevel: 'full_control',
};
render(<AutonomyStep state={stateWithFullControl} updateState={mockUpdateState} />);
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
});
});
});

View File

@@ -0,0 +1,213 @@
/**
* BasicInfoStep Component Tests
*
* Tests for the basic information step of the project wizard.
*/
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BasicInfoStep } from '@/components/projects/wizard/steps/BasicInfoStep';
import type { WizardState } from '@/components/projects/wizard/types';
describe('BasicInfoStep', () => {
const defaultState: WizardState = {
step: 1,
projectName: '',
description: '',
repoUrl: '',
complexity: null,
clientMode: null,
autonomyLevel: null,
};
const mockUpdateState = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders the step title', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText('Create New Project')).toBeInTheDocument();
});
it('renders project name input', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByLabelText(/project name/i)).toBeInTheDocument();
});
it('renders description textarea', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
});
it('renders repository URL input', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByLabelText(/repository url/i)).toBeInTheDocument();
});
it('shows required indicator for project name', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const label = screen.getByText(/project name/i);
expect(label.parentElement).toHaveTextContent('*');
});
it('shows optional indicator for description', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/description \(optional\)/i)).toBeInTheDocument();
});
});
describe('State management', () => {
it('displays current state values', () => {
const stateWithValues: WizardState = {
...defaultState,
projectName: 'My Project',
description: 'A test project',
repoUrl: 'https://github.com/test/repo',
};
render(<BasicInfoStep state={stateWithValues} updateState={mockUpdateState} />);
expect(screen.getByDisplayValue('My Project')).toBeInTheDocument();
expect(screen.getByDisplayValue('A test project')).toBeInTheDocument();
expect(screen.getByDisplayValue('https://github.com/test/repo')).toBeInTheDocument();
});
it('calls updateState when project name changes', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/project name/i);
await user.type(input, 'New Project');
expect(mockUpdateState).toHaveBeenCalled();
});
it('calls updateState when description changes', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const textarea = screen.getByLabelText(/description/i);
await user.type(textarea, 'A new description');
expect(mockUpdateState).toHaveBeenCalled();
});
it('calls updateState when repository URL changes', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/repository url/i);
await user.type(input, 'https://github.com/test/repo');
expect(mockUpdateState).toHaveBeenCalled();
});
});
describe('Validation', () => {
it('shows error for project name less than 3 characters on blur', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/project name/i);
await user.type(input, 'ab');
await user.tab(); // Trigger blur
await waitFor(() => {
expect(screen.getByText(/must be at least 3 characters/i)).toBeInTheDocument();
});
});
it('shows validation hint for repository URL', () => {
// Note: URL validation error display is limited due to the hybrid controlled/uncontrolled
// pattern where internal form state (from register) doesn't sync with controlled value.
// The empty string default passes validation since URL is optional.
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
// Should show the hint text instead of error
expect(screen.getByText(/connect an existing repository/i)).toBeInTheDocument();
});
it('accepts empty repository URL', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/repository url/i);
await user.clear(input);
await user.tab();
// Should not show error for empty URL
expect(screen.queryByText(/please enter a valid url/i)).not.toBeInTheDocument();
});
it('accepts valid repository URL', async () => {
const user = userEvent.setup();
const stateWithUrl: WizardState = {
...defaultState,
repoUrl: 'https://github.com/test/repo',
};
render(<BasicInfoStep state={stateWithUrl} updateState={mockUpdateState} />);
screen.getByLabelText(/repository url/i); // Verify field exists
await user.tab(); // Move to and away from field
expect(screen.queryByText(/please enter a valid url/i)).not.toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('has proper aria attributes for project name input', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/project name/i);
await user.type(input, 'a');
await user.tab();
await waitFor(() => {
expect(input).toHaveAttribute('aria-invalid', 'true');
});
});
it('has aria-describedby for error messages', async () => {
const user = userEvent.setup();
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
const input = screen.getByLabelText(/project name/i);
await user.type(input, 'a');
await user.tab();
await waitFor(() => {
expect(input).toHaveAttribute('aria-describedby', 'project-name-error');
});
});
it('has hint text for description', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/helps the AI agents understand/i)).toBeInTheDocument();
});
it('has hint text for repository URL', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/connect an existing repository/i)).toBeInTheDocument();
});
});
describe('Placeholders', () => {
it('shows placeholder for project name', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByPlaceholderText(/e-commerce platform/i)).toBeInTheDocument();
});
it('shows placeholder for description', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByPlaceholderText(/briefly describe/i)).toBeInTheDocument();
});
it('shows placeholder for repository URL', () => {
render(<BasicInfoStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByPlaceholderText(/github.com/i)).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,178 @@
/**
* ClientModeStep Component Tests
*
* Tests for the client interaction mode selection step.
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ClientModeStep } from '@/components/projects/wizard/steps/ClientModeStep';
import { clientModeOptions } from '@/components/projects/wizard/constants';
import type { WizardState } from '@/components/projects/wizard/types';
describe('ClientModeStep', () => {
const defaultState: WizardState = {
step: 3,
projectName: 'Test Project',
description: '',
repoUrl: '',
complexity: 'medium',
clientMode: null,
autonomyLevel: null,
};
const mockUpdateState = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders the step title', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText('How Would You Like to Work?')).toBeInTheDocument();
});
it('renders the description text', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/choose how you want to interact/i)).toBeInTheDocument();
});
it('renders all client mode options', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
clientModeOptions.forEach((option) => {
expect(screen.getByText(option.label)).toBeInTheDocument();
expect(screen.getByText(option.description)).toBeInTheDocument();
});
});
it('renders detail items for each option', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
clientModeOptions.forEach((option) => {
option.details.forEach((detail) => {
expect(screen.getByText(detail)).toBeInTheDocument();
});
});
});
it('has accessible radiogroup role', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /client interaction mode options/i })).toBeInTheDocument();
});
});
describe('Selection', () => {
it('shows no selection initially', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
// No check icons should be visible for selected state
const selectedIndicators = document.querySelectorAll('[data-selected="true"]');
expect(selectedIndicators.length).toBe(0);
});
it('calls updateState when clicking the technical mode option', async () => {
const user = userEvent.setup();
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
await user.click(technicalOption);
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
});
it('calls updateState when clicking the auto mode option', async () => {
const user = userEvent.setup();
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
const autoOption = screen.getByRole('button', { name: /auto mode.*help me figure/i });
await user.click(autoOption);
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'auto' });
});
it('shows visual selection indicator when an option is selected', () => {
const stateWithSelection: WizardState = {
...defaultState,
clientMode: 'technical',
};
render(<ClientModeStep state={stateWithSelection} updateState={mockUpdateState} />);
// The selected card should have the check icon
const checkIcons = document.querySelectorAll('.lucide-check');
expect(checkIcons.length).toBeGreaterThan(0);
});
it('highlights selected option icon for auto mode', () => {
const stateWithAuto: WizardState = {
...defaultState,
clientMode: 'auto',
};
render(<ClientModeStep state={stateWithAuto} updateState={mockUpdateState} />);
// Should have check mark for selected option
const checkIcons = document.querySelectorAll('.lucide-check');
expect(checkIcons.length).toBeGreaterThan(0);
});
});
describe('Accessibility', () => {
it('each option has accessible aria-label', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
clientModeOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
});
expect(button).toBeInTheDocument();
});
});
it('icons have aria-hidden attribute', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
const hiddenIcons = document.querySelectorAll('[aria-hidden="true"]');
expect(hiddenIcons.length).toBeGreaterThan(0);
});
it('CheckCircle2 icons in detail lists are hidden from assistive tech', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
// All lucide icons should have aria-hidden
const allCheckCircles = document.querySelectorAll('.lucide-circle-check-big');
allCheckCircles.forEach((icon) => {
expect(icon).toHaveAttribute('aria-hidden', 'true');
});
});
});
describe('Edge cases', () => {
it('allows changing selection', async () => {
const user = userEvent.setup();
const stateWithTechnical: WizardState = {
...defaultState,
clientMode: 'technical',
};
render(<ClientModeStep state={stateWithTechnical} updateState={mockUpdateState} />);
const autoOption = screen.getByRole('button', { name: /auto mode.*help me figure/i });
await user.click(autoOption);
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'auto' });
});
it('handles clicking already selected option', async () => {
const user = userEvent.setup();
const stateWithTechnical: WizardState = {
...defaultState,
clientMode: 'technical',
};
render(<ClientModeStep state={stateWithTechnical} updateState={mockUpdateState} />);
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
await user.click(technicalOption);
// Should still call updateState
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
});
});
});

View File

@@ -0,0 +1,179 @@
/**
* ComplexityStep Component Tests
*
* Tests for the project complexity selection step.
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComplexityStep } from '@/components/projects/wizard/steps/ComplexityStep';
import { complexityOptions } from '@/components/projects/wizard/constants';
import type { WizardState } from '@/components/projects/wizard/types';
describe('ComplexityStep', () => {
const defaultState: WizardState = {
step: 2,
projectName: 'Test Project',
description: '',
repoUrl: '',
complexity: null,
clientMode: null,
autonomyLevel: null,
};
const mockUpdateState = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders the step title', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText('Project Complexity')).toBeInTheDocument();
});
it('renders the description text', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByText(/how complex is your project/i)).toBeInTheDocument();
});
it('renders all complexity options', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
complexityOptions.forEach((option) => {
expect(screen.getByText(option.label)).toBeInTheDocument();
expect(screen.getByText(option.description)).toBeInTheDocument();
});
});
it('renders scope information for each option', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
complexityOptions.forEach((option) => {
expect(screen.getByText(option.scope)).toBeInTheDocument();
});
});
it('renders examples for each option', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
complexityOptions.forEach((option) => {
expect(screen.getByText(option.examples)).toBeInTheDocument();
});
});
it('has accessible radiogroup role', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /project complexity options/i })).toBeInTheDocument();
});
});
describe('Selection', () => {
it('shows no selection initially', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
// No check icons should be visible
const selectedIndicators = document.querySelectorAll('[data-selected="true"]');
expect(selectedIndicators.length).toBe(0);
});
it('calls updateState when clicking a complexity option', async () => {
const user = userEvent.setup();
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
// Find and click the "Simple" option
const simpleOption = screen.getByRole('button', { name: /simple.*small applications/i });
await user.click(simpleOption);
expect(mockUpdateState).toHaveBeenCalledWith({ complexity: 'simple' });
});
it('calls updateState when selecting script complexity', async () => {
const user = userEvent.setup();
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
const scriptOption = screen.getByRole('button', { name: /script.*single-file/i });
await user.click(scriptOption);
expect(mockUpdateState).toHaveBeenCalledWith({ complexity: 'script' });
});
it('calls updateState when selecting medium complexity', async () => {
const user = userEvent.setup();
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
const mediumOption = screen.getByRole('button', { name: /medium.*full applications/i });
await user.click(mediumOption);
expect(mockUpdateState).toHaveBeenCalledWith({ complexity: 'medium' });
});
it('calls updateState when selecting complex complexity', async () => {
const user = userEvent.setup();
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
const complexOption = screen.getByRole('button', { name: /complex.*enterprise/i });
await user.click(complexOption);
expect(mockUpdateState).toHaveBeenCalledWith({ complexity: 'complex' });
});
it('shows visual selection indicator when an option is selected', () => {
const stateWithSelection: WizardState = {
...defaultState,
complexity: 'simple',
};
render(<ComplexityStep state={stateWithSelection} updateState={mockUpdateState} />);
// The selected card should have the check icon
const checkIcons = document.querySelectorAll('.lucide-check');
expect(checkIcons.length).toBeGreaterThan(0);
});
});
describe('Script Mode Hint', () => {
it('does not show script mode hint when script is not selected', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.queryByText(/simplified flow/i)).not.toBeInTheDocument();
});
it('shows script mode hint when script complexity is selected', () => {
const stateWithScript: WizardState = {
...defaultState,
complexity: 'script',
};
render(<ComplexityStep state={stateWithScript} updateState={mockUpdateState} />);
expect(screen.getByText(/simplified flow/i)).toBeInTheDocument();
expect(screen.getByText(/skip to agent chat/i)).toBeInTheDocument();
});
it('does not show script mode hint for simple complexity', () => {
const stateWithSimple: WizardState = {
...defaultState,
complexity: 'simple',
};
render(<ComplexityStep state={stateWithSimple} updateState={mockUpdateState} />);
expect(screen.queryByText(/simplified flow/i)).not.toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('each option has accessible aria-label', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
complexityOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
});
expect(button).toBeInTheDocument();
});
});
it('icons have aria-hidden attribute', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
const hiddenIcons = document.querySelectorAll('[aria-hidden="true"]');
expect(hiddenIcons.length).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,259 @@
/**
* ReviewStep Component Tests
*
* Tests for the project review/summary step.
*/
import { render, screen } from '@testing-library/react';
import { ReviewStep } from '@/components/projects/wizard/steps/ReviewStep';
import type { WizardState } from '@/components/projects/wizard/types';
describe('ReviewStep', () => {
const completeState: WizardState = {
step: 6,
projectName: 'My Test Project',
description: 'A comprehensive test project description',
repoUrl: 'https://github.com/test/repo',
complexity: 'medium',
clientMode: 'technical',
autonomyLevel: 'milestone',
};
const minimalState: WizardState = {
step: 6,
projectName: '',
description: '',
repoUrl: '',
complexity: null,
clientMode: null,
autonomyLevel: null,
};
const scriptState: WizardState = {
step: 6,
projectName: 'Quick Script',
description: 'A simple script',
repoUrl: '',
complexity: 'script',
clientMode: null,
autonomyLevel: null,
};
describe('Rendering', () => {
it('renders the step title', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Review Your Project')).toBeInTheDocument();
});
it('renders the description text', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText(/please review your selections/i)).toBeInTheDocument();
});
});
describe('Basic Information Card', () => {
it('displays project name', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Project Name')).toBeInTheDocument();
expect(screen.getByText('My Test Project')).toBeInTheDocument();
});
it('displays description', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('A comprehensive test project description')).toBeInTheDocument();
});
it('displays repository URL', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Repository')).toBeInTheDocument();
expect(screen.getByText('https://github.com/test/repo')).toBeInTheDocument();
});
it('shows "Not specified" for empty project name', () => {
render(<ReviewStep state={minimalState} />);
expect(screen.getByText('Not specified')).toBeInTheDocument();
});
it('shows "No description provided" for empty description', () => {
render(<ReviewStep state={minimalState} />);
expect(screen.getByText('No description provided')).toBeInTheDocument();
});
it('shows "New repository will be created" for empty repo URL', () => {
render(<ReviewStep state={minimalState} />);
expect(screen.getByText('New repository will be created')).toBeInTheDocument();
});
});
describe('Complexity Card', () => {
it('displays complexity card title', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Project Complexity')).toBeInTheDocument();
});
it('displays selected complexity with details', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Medium')).toBeInTheDocument();
});
it('shows "Not selected" when no complexity chosen', () => {
render(<ReviewStep state={minimalState} />);
const notSelectedTexts = screen.getAllByText('Not selected');
expect(notSelectedTexts.length).toBeGreaterThan(0);
});
it('displays Script complexity correctly', () => {
render(<ReviewStep state={scriptState} />);
expect(screen.getByText('Script')).toBeInTheDocument();
});
});
describe('Interaction Mode Card', () => {
it('displays interaction mode card title', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Interaction Mode')).toBeInTheDocument();
});
it('displays selected client mode with details', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Technical Mode')).toBeInTheDocument();
});
it('shows "Not selected" when no client mode chosen', () => {
render(<ReviewStep state={minimalState} />);
// Find within the Interaction Mode card context
const cards = screen.getAllByText('Not selected');
expect(cards.length).toBeGreaterThan(0);
});
it('shows auto mode message for script projects', () => {
render(<ReviewStep state={scriptState} />);
const autoModeTexts = screen.getAllByText(/automatically set for script/i);
expect(autoModeTexts.length).toBeGreaterThan(0);
});
});
describe('Autonomy Level Card', () => {
it('displays autonomy level card title', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Autonomy Level')).toBeInTheDocument();
});
it('displays selected autonomy level with details', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Milestone')).toBeInTheDocument();
});
it('shows "Not selected" when no autonomy level chosen', () => {
render(<ReviewStep state={minimalState} />);
const cards = screen.getAllByText('Not selected');
expect(cards.length).toBeGreaterThan(0);
});
it('shows autonomous message for script projects', () => {
render(<ReviewStep state={scriptState} />);
const autonomousMessages = screen.getAllByText(/autonomous/i);
expect(autonomousMessages.length).toBeGreaterThan(0);
const scriptMessages = screen.getAllByText(/automatically set for script/i);
expect(scriptMessages.length).toBeGreaterThan(0);
});
});
describe('Ready to Create Card', () => {
it('displays the ready to create card', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Ready to Create')).toBeInTheDocument();
});
it('displays the summary message', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText(/once you create this project/i)).toBeInTheDocument();
expect(screen.getByText(/product owner agent/i)).toBeInTheDocument();
});
});
describe('Script Mode Display', () => {
it('shows script complexity selected', () => {
render(<ReviewStep state={scriptState} />);
expect(screen.getByText('Script')).toBeInTheDocument();
});
it('auto-fills interaction mode for script projects', () => {
render(<ReviewStep state={scriptState} />);
// Auto Mode text appears for script projects
const autoModeTexts = screen.getAllByText(/automatically set for script/i);
expect(autoModeTexts.length).toBeGreaterThan(0);
});
it('auto-fills autonomy level for script projects', () => {
render(<ReviewStep state={scriptState} />);
// Autonomous text appears for script projects
const autonomousTexts = screen.getAllByText(/autonomous.*automatically set for script/i);
expect(autonomousTexts.length).toBeGreaterThan(0);
});
});
describe('Different Selections', () => {
it('displays Simple complexity', () => {
const simpleState: WizardState = {
...completeState,
complexity: 'simple',
};
render(<ReviewStep state={simpleState} />);
expect(screen.getByText('Simple')).toBeInTheDocument();
});
it('displays Complex complexity', () => {
const complexState: WizardState = {
...completeState,
complexity: 'complex',
};
render(<ReviewStep state={complexState} />);
expect(screen.getByText('Complex')).toBeInTheDocument();
});
it('displays Auto Mode client mode', () => {
const autoState: WizardState = {
...completeState,
clientMode: 'auto',
};
render(<ReviewStep state={autoState} />);
expect(screen.getByText('Auto Mode')).toBeInTheDocument();
});
it('displays Full Control autonomy', () => {
const fullControlState: WizardState = {
...completeState,
autonomyLevel: 'full_control',
};
render(<ReviewStep state={fullControlState} />);
expect(screen.getByText('Full Control')).toBeInTheDocument();
});
it('displays Autonomous autonomy level', () => {
const autonomousState: WizardState = {
...completeState,
autonomyLevel: 'autonomous',
};
render(<ReviewStep state={autonomousState} />);
expect(screen.getByText('Autonomous')).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('icons have aria-hidden attribute', () => {
render(<ReviewStep state={completeState} />);
const hiddenIcons = document.querySelectorAll('[aria-hidden="true"]');
expect(hiddenIcons.length).toBeGreaterThan(0);
});
it('renders card titles as headings', () => {
render(<ReviewStep state={completeState} />);
expect(screen.getByText('Basic Information')).toBeInTheDocument();
expect(screen.getByText('Project Complexity')).toBeInTheDocument();
expect(screen.getByText('Interaction Mode')).toBeInTheDocument();
expect(screen.getByText('Autonomy Level')).toBeInTheDocument();
});
});
});