forked from cardosofelipe/fast-next-template
- 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`.
329 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|