/**
* 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 }) => (
)),
ComplexityStep: jest.fn(({ updateState }) => (
)),
ClientModeStep: jest.fn(() => ),
AutonomyStep: jest.fn(() => ),
AgentChatStep: jest.fn(() => ),
ReviewStep: jest.fn(({ state }) => (
Project: {state.projectName}
)),
}));
// Mock StepIndicator
jest.mock('@/components/projects/wizard/StepIndicator', () => ({
StepIndicator: jest.fn(({ currentStep, isScriptMode }) => (
Step {currentStep} {isScriptMode && '(script mode)'}
)),
}));
// 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,
})),
}));
// Import mock from next-intl/navigation mock (used by @/lib/i18n/routing)
import { mockPush } from 'next-intl/navigation';
// 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 }) => (
{children}
);
}
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(, { wrapper: createWrapper() });
expect(screen.getByTestId('step-indicator')).toBeInTheDocument();
});
it('renders BasicInfoStep on step 1', () => {
mockWizardState.step = 1;
render(, { wrapper: createWrapper() });
expect(screen.getByTestId('basic-info-step')).toBeInTheDocument();
});
it('renders ComplexityStep on step 2', () => {
mockWizardState.step = 2;
render(, { wrapper: createWrapper() });
expect(screen.getByTestId('complexity-step')).toBeInTheDocument();
});
it('renders AgentChatStep on step 5', () => {
mockWizardState.step = 5;
render(, { wrapper: createWrapper() });
expect(screen.getByTestId('agent-chat-step')).toBeInTheDocument();
});
it('renders ReviewStep on step 6', () => {
mockWizardState.step = 6;
render(, { wrapper: createWrapper() });
expect(screen.getByTestId('review-step')).toBeInTheDocument();
});
it('applies custom className', () => {
const { container } = render(, {
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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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(, { 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 }));
// Locale-aware router adds locale prefix automatically
expect(mockPush).toHaveBeenCalledWith('/projects/test-project');
});
it('allows creating another project', async () => {
const user = userEvent.setup();
mockWizardState.step = 6;
render(, { 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(, { 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(, { wrapper: createWrapper() });
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled();
});
it('enables Next button when can proceed', () => {
mockWizardState.projectName = 'Valid Name';
render(, { 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(, { wrapper: createWrapper() });
await user.click(screen.getByRole('button', { name: /create project/i }));
expect(screen.getByText(/creating/i)).toBeInTheDocument();
});
});
});