Files
syndarix/frontend/tests/components/projects/wizard/ProjectWizard.test.tsx
Felipe Cardoso da5affd613 fix(frontend): remove locale-dependent routing and migrate to centralized locale-aware router
- Replaced `next/navigation` with `@/lib/i18n/routing` across components, pages, and tests.
- Removed redundant `locale` props from `ProjectWizard` and related pages.
- Updated navigation to exclude explicit `locale` in paths.
- Refactored tests to use mocks from `next-intl/navigation`.
2026-01-03 01:34:53 +01:00

329 lines
11 KiB
TypeScript

/**
* 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,
})),
}));
// 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 }) => (
<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 />, { wrapper: createWrapper() });
expect(screen.getByTestId('step-indicator')).toBeInTheDocument();
});
it('renders BasicInfoStep on step 1', () => {
mockWizardState.step = 1;
render(<ProjectWizard />, { wrapper: createWrapper() });
expect(screen.getByTestId('basic-info-step')).toBeInTheDocument();
});
it('renders ComplexityStep on step 2', () => {
mockWizardState.step = 2;
render(<ProjectWizard />, { wrapper: createWrapper() });
expect(screen.getByTestId('complexity-step')).toBeInTheDocument();
});
it('renders AgentChatStep on step 5', () => {
mockWizardState.step = 5;
render(<ProjectWizard />, { wrapper: createWrapper() });
expect(screen.getByTestId('agent-chat-step')).toBeInTheDocument();
});
it('renders ReviewStep on step 6', () => {
mockWizardState.step = 6;
render(<ProjectWizard />, { wrapper: createWrapper() });
expect(screen.getByTestId('review-step')).toBeInTheDocument();
});
it('applies custom className', () => {
const { container } = render(<ProjectWizard 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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 />, { 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(<ProjectWizard />, { 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 />, { 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 />, { wrapper: createWrapper() });
expect(screen.getByRole('button', { name: /next/i })).toBeDisabled();
});
it('enables Next button when can proceed', () => {
mockWizardState.projectName = 'Valid Name';
render(<ProjectWizard />, { 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 />, { wrapper: createWrapper() });
await user.click(screen.getByRole('button', { name: /create project/i }));
expect(screen.getByText(/creating/i)).toBeInTheDocument();
});
});
});