forked from cardosofelipe/fast-next-template
Implement the main dashboard / projects list page for Syndarix as the landing page after login. The implementation includes: Dashboard Components: - QuickStats: Overview cards showing active projects, agents, issues, approvals - ProjectsSection: Grid/list view with filtering and sorting controls - ProjectCardGrid: Rich project cards for grid view - ProjectRowList: Compact rows for list view - ActivityFeed: Real-time activity sidebar with connection status - PerformanceCard: Performance metrics display - EmptyState: Call-to-action for new users - ProjectStatusBadge: Status indicator with icons - ComplexityIndicator: Visual complexity dots - ProgressBar: Accessible progress bar component Features: - Projects grid/list view with view mode toggle - Filter by status (all, active, paused, completed, archived) - Sort by recent, name, progress, or issues - Quick stats overview with counts - Real-time activity feed sidebar with live/reconnecting status - Performance metrics card - Create project button linking to wizard - Responsive layout for mobile/desktop - Loading skeleton states - Empty state for new users API Integration: - useProjects hook for fetching projects (mock data until backend ready) - useDashboardStats hook for statistics - TanStack Query for caching and data fetching Testing: - 37 unit tests covering all dashboard components - E2E test suite for dashboard functionality - Accessibility tests (keyboard nav, aria attributes, heading hierarchy) Technical: - TypeScript strict mode compliance - ESLint passing - WCAG AA accessibility compliance - Mobile-first responsive design - Dark mode support via semantic tokens - Follows design system guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
358 lines
9.7 KiB
TypeScript
358 lines
9.7 KiB
TypeScript
/**
|
|
* Tests for useWizardState hook
|
|
*/
|
|
|
|
import { renderHook, act } from '@testing-library/react';
|
|
import { useWizardState } from '@/components/projects/wizard/useWizardState';
|
|
import { WIZARD_STEPS } from '@/components/projects/wizard/constants';
|
|
|
|
describe('useWizardState', () => {
|
|
describe('initial state', () => {
|
|
it('starts at step 1', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
expect(result.current.state.step).toBe(1);
|
|
});
|
|
|
|
it('has empty form fields', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
expect(result.current.state.projectName).toBe('');
|
|
expect(result.current.state.description).toBe('');
|
|
expect(result.current.state.repoUrl).toBe('');
|
|
});
|
|
|
|
it('has null selections', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
expect(result.current.state.complexity).toBeNull();
|
|
expect(result.current.state.clientMode).toBeNull();
|
|
expect(result.current.state.autonomyLevel).toBeNull();
|
|
});
|
|
|
|
it('is not in script mode', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
expect(result.current.isScriptMode).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateState', () => {
|
|
it('updates project name', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'Test Project' });
|
|
});
|
|
expect(result.current.state.projectName).toBe('Test Project');
|
|
});
|
|
|
|
it('updates multiple fields at once', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test',
|
|
description: 'A test project',
|
|
});
|
|
});
|
|
expect(result.current.state.projectName).toBe('Test');
|
|
expect(result.current.state.description).toBe('A test project');
|
|
});
|
|
});
|
|
|
|
describe('resetState', () => {
|
|
it('resets to initial state', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
// Make some changes
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test',
|
|
complexity: 'medium',
|
|
step: 3,
|
|
});
|
|
});
|
|
|
|
// Reset
|
|
act(() => {
|
|
result.current.resetState();
|
|
});
|
|
|
|
expect(result.current.state.projectName).toBe('');
|
|
expect(result.current.state.complexity).toBeNull();
|
|
expect(result.current.state.step).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('canProceed', () => {
|
|
it('requires project name at least 3 chars for step 1', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
expect(result.current.canProceed).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'AB' });
|
|
});
|
|
expect(result.current.canProceed).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'ABC' });
|
|
});
|
|
expect(result.current.canProceed).toBe(true);
|
|
});
|
|
|
|
it('requires complexity selection for step 2', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
// Move to step 2
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'Test', step: 2 });
|
|
});
|
|
|
|
expect(result.current.canProceed).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.updateState({ complexity: 'medium' });
|
|
});
|
|
expect(result.current.canProceed).toBe(true);
|
|
});
|
|
|
|
it('requires client mode selection for step 3 (non-script)', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test',
|
|
complexity: 'medium',
|
|
step: 3,
|
|
});
|
|
});
|
|
|
|
expect(result.current.canProceed).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.updateState({ clientMode: 'technical' });
|
|
});
|
|
expect(result.current.canProceed).toBe(true);
|
|
});
|
|
|
|
it('requires autonomy level for step 4 (non-script)', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test',
|
|
complexity: 'medium',
|
|
clientMode: 'auto',
|
|
step: 4,
|
|
});
|
|
});
|
|
|
|
expect(result.current.canProceed).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.updateState({ autonomyLevel: 'milestone' });
|
|
});
|
|
expect(result.current.canProceed).toBe(true);
|
|
});
|
|
|
|
it('always allows proceeding from step 5 (agent chat)', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({ step: 5 });
|
|
});
|
|
|
|
expect(result.current.canProceed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('navigation', () => {
|
|
it('goNext increments step', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'Test Project' });
|
|
});
|
|
|
|
act(() => {
|
|
result.current.goNext();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(2);
|
|
});
|
|
|
|
it('goBack decrements step', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'Test', step: 3 });
|
|
});
|
|
|
|
act(() => {
|
|
result.current.goBack();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(2);
|
|
});
|
|
|
|
it('goBack does nothing at step 1', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.goBack();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(1);
|
|
});
|
|
|
|
it('does not proceed when canProceed is false', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
// Project name too short
|
|
act(() => {
|
|
result.current.updateState({ projectName: 'AB' });
|
|
});
|
|
|
|
act(() => {
|
|
result.current.goNext();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('script mode', () => {
|
|
it('sets isScriptMode when complexity is script', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({ complexity: 'script' });
|
|
});
|
|
|
|
expect(result.current.isScriptMode).toBe(true);
|
|
});
|
|
|
|
it('skips from step 2 to step 5 for scripts', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
// Set up step 2 with script complexity
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test Script',
|
|
step: 2,
|
|
complexity: 'script',
|
|
});
|
|
});
|
|
|
|
// Go next should skip to step 5
|
|
act(() => {
|
|
result.current.goNext();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(WIZARD_STEPS.AGENT_CHAT);
|
|
});
|
|
|
|
it('auto-sets clientMode and autonomyLevel for scripts', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test Script',
|
|
step: 2,
|
|
complexity: 'script',
|
|
});
|
|
});
|
|
|
|
act(() => {
|
|
result.current.goNext();
|
|
});
|
|
|
|
expect(result.current.state.clientMode).toBe('auto');
|
|
expect(result.current.state.autonomyLevel).toBe('autonomous');
|
|
});
|
|
|
|
it('goBack from step 5 goes to step 2 for scripts', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test Script',
|
|
complexity: 'script',
|
|
step: 5,
|
|
});
|
|
});
|
|
|
|
act(() => {
|
|
result.current.goBack();
|
|
});
|
|
|
|
expect(result.current.state.step).toBe(WIZARD_STEPS.COMPLEXITY);
|
|
});
|
|
});
|
|
|
|
describe('getProjectData', () => {
|
|
it('generates correct project data', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'My Test Project',
|
|
description: 'A description',
|
|
repoUrl: 'https://github.com/test/repo',
|
|
complexity: 'medium',
|
|
clientMode: 'technical',
|
|
autonomyLevel: 'milestone',
|
|
});
|
|
});
|
|
|
|
const data = result.current.getProjectData();
|
|
|
|
expect(data.name).toBe('My Test Project');
|
|
expect(data.slug).toBe('my-test-project');
|
|
expect(data.description).toBe('A description');
|
|
expect(data.autonomy_level).toBe('milestone');
|
|
expect(data.settings.complexity).toBe('medium');
|
|
expect(data.settings.client_mode).toBe('technical');
|
|
expect(data.settings.repo_url).toBe('https://github.com/test/repo');
|
|
});
|
|
|
|
it('generates URL-safe slug', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'My Project! With Special @#$ Characters',
|
|
});
|
|
});
|
|
|
|
const data = result.current.getProjectData();
|
|
expect(data.slug).toBe('my-project-with-special-characters');
|
|
});
|
|
|
|
it('excludes empty repoUrl from settings', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test Project',
|
|
repoUrl: '',
|
|
});
|
|
});
|
|
|
|
const data = result.current.getProjectData();
|
|
expect(data.settings.repo_url).toBeUndefined();
|
|
});
|
|
|
|
it('uses defaults for null values', () => {
|
|
const { result } = renderHook(() => useWizardState());
|
|
|
|
act(() => {
|
|
result.current.updateState({
|
|
projectName: 'Test Project',
|
|
});
|
|
});
|
|
|
|
const data = result.current.getProjectData();
|
|
expect(data.autonomy_level).toBe('milestone');
|
|
expect(data.settings.complexity).toBe('medium');
|
|
expect(data.settings.client_mode).toBe('auto');
|
|
});
|
|
});
|
|
});
|