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:
330
frontend/tests/components/projects/wizard/ProjectWizard.test.tsx
Normal file
330
frontend/tests/components/projects/wizard/ProjectWizard.test.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* ProjectWizard Component Tests
|
||||
*
|
||||
* Tests for the main project creation wizard component.
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ProjectWizard } from '@/components/projects/wizard/ProjectWizard';
|
||||
|
||||
// Mock step components
|
||||
jest.mock('@/components/projects/wizard/steps', () => ({
|
||||
BasicInfoStep: jest.fn(({ updateState }) => (
|
||||
<div data-testid="basic-info-step">
|
||||
<button onClick={() => updateState({ projectName: 'Test Project' })}>Set Name</button>
|
||||
</div>
|
||||
)),
|
||||
ComplexityStep: jest.fn(({ updateState }) => (
|
||||
<div data-testid="complexity-step">
|
||||
<button onClick={() => updateState({ complexity: 'simple' })}>Set Simple</button>
|
||||
<button onClick={() => updateState({ complexity: 'script' })}>Set Script</button>
|
||||
</div>
|
||||
)),
|
||||
ClientModeStep: jest.fn(() => <div data-testid="client-mode-step" />),
|
||||
AutonomyStep: jest.fn(() => <div data-testid="autonomy-step" />),
|
||||
AgentChatStep: jest.fn(() => <div data-testid="agent-chat-step" />),
|
||||
ReviewStep: jest.fn(({ state }) => (
|
||||
<div data-testid="review-step">
|
||||
<span>Project: {state.projectName}</span>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock StepIndicator
|
||||
jest.mock('@/components/projects/wizard/StepIndicator', () => ({
|
||||
StepIndicator: jest.fn(({ currentStep, isScriptMode }) => (
|
||||
<div data-testid="step-indicator">
|
||||
Step {currentStep} {isScriptMode && '(script mode)'}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock useWizardState hook
|
||||
const mockUpdateState = jest.fn();
|
||||
const mockResetState = jest.fn();
|
||||
const mockGoNext = jest.fn();
|
||||
const mockGoBack = jest.fn();
|
||||
const mockGetProjectData = jest.fn(() => ({
|
||||
name: 'Test Project',
|
||||
slug: 'test-project',
|
||||
description: 'Test description',
|
||||
autonomy_level: 'milestone',
|
||||
settings: {},
|
||||
}));
|
||||
|
||||
let mockWizardState = {
|
||||
step: 1 as 1 | 2 | 3 | 4 | 5 | 6,
|
||||
projectName: 'Test Project',
|
||||
description: '',
|
||||
repoUrl: '',
|
||||
complexity: null as string | null,
|
||||
clientMode: null as string | null,
|
||||
autonomyLevel: null as string | null,
|
||||
};
|
||||
|
||||
jest.mock('@/components/projects/wizard/useWizardState', () => ({
|
||||
useWizardState: jest.fn(() => ({
|
||||
state: mockWizardState,
|
||||
updateState: mockUpdateState,
|
||||
resetState: mockResetState,
|
||||
isScriptMode: mockWizardState.complexity === 'script',
|
||||
canProceed: mockWizardState.projectName.length > 0,
|
||||
goNext: mockGoNext,
|
||||
goBack: mockGoBack,
|
||||
getProjectData: mockGetProjectData,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock router
|
||||
const mockPush = jest.fn();
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock API client
|
||||
const mockPost = jest.fn();
|
||||
jest.mock('@/lib/api/client', () => ({
|
||||
apiClient: {
|
||||
instance: {
|
||||
post: jest.fn((url, data) => mockPost(url, data)),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ProjectWizard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockWizardState = {
|
||||
step: 1,
|
||||
projectName: 'Test Project',
|
||||
description: '',
|
||||
repoUrl: '',
|
||||
complexity: null,
|
||||
clientMode: null,
|
||||
autonomyLevel: null,
|
||||
};
|
||||
mockPost.mockResolvedValue({
|
||||
data: {
|
||||
id: 'project-123',
|
||||
name: 'Test Project',
|
||||
slug: 'test-project',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders the step indicator', () => {
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByTestId('step-indicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders BasicInfoStep on step 1', () => {
|
||||
mockWizardState.step = 1;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByTestId('basic-info-step')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ComplexityStep on step 2', () => {
|
||||
mockWizardState.step = 2;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByTestId('complexity-step')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders AgentChatStep on step 5', () => {
|
||||
mockWizardState.step = 5;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByTestId('agent-chat-step')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ReviewStep on step 6', () => {
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByTestId('review-step')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<ProjectWizard locale="en" className="custom-class" />, {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('calls goNext when Next button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 1;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
expect(mockGoNext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls goBack when Back button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 2;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /back/i }));
|
||||
expect(mockGoBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides Back button on step 1', () => {
|
||||
mockWizardState.step = 1;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
const backButton = screen.getByRole('button', { name: /back/i });
|
||||
expect(backButton).toHaveClass('invisible');
|
||||
});
|
||||
|
||||
it('shows Back button visible on step 2', () => {
|
||||
mockWizardState.step = 2;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
const backButton = screen.getByRole('button', { name: /back/i });
|
||||
expect(backButton).not.toHaveClass('invisible');
|
||||
});
|
||||
|
||||
it('shows Create Project button on review step', () => {
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByRole('button', { name: /create project/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Script Mode', () => {
|
||||
it('skips client mode step in script mode', () => {
|
||||
mockWizardState.step = 3;
|
||||
mockWizardState.complexity = 'script';
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
// ClientModeStep should not render for script mode
|
||||
expect(screen.queryByTestId('client-mode-step')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('skips autonomy step in script mode', () => {
|
||||
mockWizardState.step = 4;
|
||||
mockWizardState.complexity = 'script';
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
// AutonomyStep should not render for script mode
|
||||
expect(screen.queryByTestId('autonomy-step')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows script mode indicator', () => {
|
||||
mockWizardState.complexity = 'script';
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByText(/script mode/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Project Creation', () => {
|
||||
it('shows success screen after creation', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/project created successfully/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays project name in success message', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/test project/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to project dashboard on success', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /go to project dashboard/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /go to project dashboard/i }));
|
||||
expect(mockPush).toHaveBeenCalledWith('/en/projects/test-project');
|
||||
});
|
||||
|
||||
it('allows creating another project', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /create another project/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create another project/i }));
|
||||
expect(mockResetState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('shows error message on creation failure', async () => {
|
||||
mockPost.mockRejectedValue(new Error('Network error'));
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to create project/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button States', () => {
|
||||
it('disables Next button when cannot proceed', () => {
|
||||
mockWizardState.projectName = '';
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Next button when can proceed', () => {
|
||||
mockWizardState.projectName = 'Valid Name';
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows loading state during creation', async () => {
|
||||
mockPost.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ data: {} }), 1000))
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
mockWizardState.step = 6;
|
||||
render(<ProjectWizard locale="en" />, { wrapper: createWrapper() });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create project/i }));
|
||||
expect(screen.getByText(/creating/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user