feat(frontend): implement main dashboard page (#48)
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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Tests for SelectableCard component
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { SelectableCard } from '@/components/projects/wizard/SelectableCard';
|
||||
|
||||
describe('SelectableCard', () => {
|
||||
const defaultProps = {
|
||||
selected: false,
|
||||
onClick: jest.fn(),
|
||||
children: <span>Card Content</span>,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
render(<SelectableCard {...defaultProps} />);
|
||||
expect(screen.getByText('Card Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when clicked', () => {
|
||||
render(<SelectableCard {...defaultProps} />);
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('has aria-pressed false when not selected', () => {
|
||||
render(<SelectableCard {...defaultProps} />);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
|
||||
it('has aria-pressed true when selected', () => {
|
||||
render(<SelectableCard {...defaultProps} selected={true} />);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
it('applies custom aria-label', () => {
|
||||
render(<SelectableCard {...defaultProps} aria-label="Select option A" />);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Select option A');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<SelectableCard {...defaultProps} className="my-custom-class" />);
|
||||
expect(screen.getByRole('button')).toHaveClass('my-custom-class');
|
||||
});
|
||||
|
||||
it('applies selected styles when selected', () => {
|
||||
const { rerender } = render(<SelectableCard {...defaultProps} />);
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
expect(button).toHaveClass('border-border');
|
||||
expect(button).not.toHaveClass('border-primary');
|
||||
|
||||
rerender(<SelectableCard {...defaultProps} selected={true} />);
|
||||
expect(button).toHaveClass('border-primary');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Tests for StepIndicator component
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { StepIndicator } from '@/components/projects/wizard/StepIndicator';
|
||||
|
||||
describe('StepIndicator', () => {
|
||||
describe('non-script mode (6 steps)', () => {
|
||||
it('renders correct step count', () => {
|
||||
render(<StepIndicator currentStep={1} isScriptMode={false} />);
|
||||
expect(screen.getByText('Step 1 of 6')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct step label for each step', () => {
|
||||
const { rerender } = render(<StepIndicator currentStep={1} isScriptMode={false} />);
|
||||
expect(screen.getByText('Basic Info')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={2} isScriptMode={false} />);
|
||||
expect(screen.getByText('Complexity')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={3} isScriptMode={false} />);
|
||||
expect(screen.getByText('Client Mode')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={4} isScriptMode={false} />);
|
||||
expect(screen.getByText('Autonomy')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={5} isScriptMode={false} />);
|
||||
expect(screen.getByText('Agent Chat')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={6} isScriptMode={false} />);
|
||||
expect(screen.getByText('Review')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders 6 progress segments', () => {
|
||||
render(<StepIndicator currentStep={3} isScriptMode={false} />);
|
||||
const progressbar = screen.getByRole('progressbar');
|
||||
expect(progressbar).toBeInTheDocument();
|
||||
expect(progressbar).toHaveAttribute('aria-valuenow', '3');
|
||||
expect(progressbar).toHaveAttribute('aria-valuemax', '6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('script mode (4 steps)', () => {
|
||||
it('renders correct step count', () => {
|
||||
render(<StepIndicator currentStep={1} isScriptMode={true} />);
|
||||
expect(screen.getByText('Step 1 of 4')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct step labels (no Client Mode or Autonomy)', () => {
|
||||
const { rerender } = render(<StepIndicator currentStep={1} isScriptMode={true} />);
|
||||
expect(screen.getByText('Basic Info')).toBeInTheDocument();
|
||||
|
||||
rerender(<StepIndicator currentStep={2} isScriptMode={true} />);
|
||||
expect(screen.getByText('Complexity')).toBeInTheDocument();
|
||||
|
||||
// Step 5 (Agent Chat) maps to display step 3
|
||||
rerender(<StepIndicator currentStep={5} isScriptMode={true} />);
|
||||
expect(screen.getByText('Step 3 of 4')).toBeInTheDocument();
|
||||
expect(screen.getByText('Agent Chat')).toBeInTheDocument();
|
||||
|
||||
// Step 6 (Review) maps to display step 4
|
||||
rerender(<StepIndicator currentStep={6} isScriptMode={true} />);
|
||||
expect(screen.getByText('Step 4 of 4')).toBeInTheDocument();
|
||||
expect(screen.getByText('Review')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders 4 progress segments', () => {
|
||||
render(<StepIndicator currentStep={5} isScriptMode={true} />);
|
||||
const progressbar = screen.getByRole('progressbar');
|
||||
expect(progressbar).toHaveAttribute('aria-valuemax', '4');
|
||||
});
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<StepIndicator currentStep={1} isScriptMode={false} className="my-custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('my-custom-class');
|
||||
});
|
||||
});
|
||||
154
frontend/tests/components/projects/wizard/constants.test.ts
Normal file
154
frontend/tests/components/projects/wizard/constants.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Tests for wizard constants and utility functions
|
||||
*/
|
||||
|
||||
import {
|
||||
complexityOptions,
|
||||
clientModeOptions,
|
||||
autonomyOptions,
|
||||
getTotalSteps,
|
||||
getStepLabels,
|
||||
getDisplayStep,
|
||||
WIZARD_STEPS,
|
||||
} from '@/components/projects/wizard/constants';
|
||||
|
||||
describe('complexityOptions', () => {
|
||||
it('has 4 options', () => {
|
||||
expect(complexityOptions).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('includes script with skipConfig: true', () => {
|
||||
const script = complexityOptions.find((o) => o.id === 'script');
|
||||
expect(script).toBeDefined();
|
||||
expect(script?.skipConfig).toBe(true);
|
||||
});
|
||||
|
||||
it('has other options with skipConfig: false', () => {
|
||||
const others = complexityOptions.filter((o) => o.id !== 'script');
|
||||
expect(others.every((o) => o.skipConfig === false)).toBe(true);
|
||||
});
|
||||
|
||||
it('has correct timelines', () => {
|
||||
const script = complexityOptions.find((o) => o.id === 'script');
|
||||
const simple = complexityOptions.find((o) => o.id === 'simple');
|
||||
const medium = complexityOptions.find((o) => o.id === 'medium');
|
||||
const complex = complexityOptions.find((o) => o.id === 'complex');
|
||||
|
||||
expect(script?.scope).toContain('Minutes to 1-2 hours');
|
||||
expect(simple?.scope).toContain('2-3 days');
|
||||
expect(medium?.scope).toContain('2-3 weeks');
|
||||
expect(complex?.scope).toContain('2-3 months');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clientModeOptions', () => {
|
||||
it('has 2 options', () => {
|
||||
expect(clientModeOptions).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('includes technical and auto modes', () => {
|
||||
const ids = clientModeOptions.map((o) => o.id);
|
||||
expect(ids).toContain('technical');
|
||||
expect(ids).toContain('auto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('autonomyOptions', () => {
|
||||
it('has 3 options', () => {
|
||||
expect(autonomyOptions).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('includes all autonomy levels', () => {
|
||||
const ids = autonomyOptions.map((o) => o.id);
|
||||
expect(ids).toContain('full_control');
|
||||
expect(ids).toContain('milestone');
|
||||
expect(ids).toContain('autonomous');
|
||||
});
|
||||
|
||||
it('has valid approval matrices', () => {
|
||||
autonomyOptions.forEach((option) => {
|
||||
expect(option.approvals).toHaveProperty('codeChanges');
|
||||
expect(option.approvals).toHaveProperty('issueUpdates');
|
||||
expect(option.approvals).toHaveProperty('architectureDecisions');
|
||||
expect(option.approvals).toHaveProperty('sprintPlanning');
|
||||
expect(option.approvals).toHaveProperty('deployments');
|
||||
});
|
||||
});
|
||||
|
||||
it('full_control requires all approvals', () => {
|
||||
const fullControl = autonomyOptions.find((o) => o.id === 'full_control');
|
||||
expect(Object.values(fullControl!.approvals).every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('autonomous only requires architecture and deployments', () => {
|
||||
const autonomous = autonomyOptions.find((o) => o.id === 'autonomous');
|
||||
expect(autonomous!.approvals.codeChanges).toBe(false);
|
||||
expect(autonomous!.approvals.issueUpdates).toBe(false);
|
||||
expect(autonomous!.approvals.architectureDecisions).toBe(true);
|
||||
expect(autonomous!.approvals.sprintPlanning).toBe(false);
|
||||
expect(autonomous!.approvals.deployments).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalSteps', () => {
|
||||
it('returns 6 for non-script mode', () => {
|
||||
expect(getTotalSteps(false)).toBe(6);
|
||||
});
|
||||
|
||||
it('returns 4 for script mode', () => {
|
||||
expect(getTotalSteps(true)).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStepLabels', () => {
|
||||
it('returns 6 labels for non-script mode', () => {
|
||||
const labels = getStepLabels(false);
|
||||
expect(labels).toHaveLength(6);
|
||||
expect(labels).toEqual([
|
||||
'Basic Info',
|
||||
'Complexity',
|
||||
'Client Mode',
|
||||
'Autonomy',
|
||||
'Agent Chat',
|
||||
'Review',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns 4 labels for script mode (no Client Mode or Autonomy)', () => {
|
||||
const labels = getStepLabels(true);
|
||||
expect(labels).toHaveLength(4);
|
||||
expect(labels).toEqual(['Basic Info', 'Complexity', 'Agent Chat', 'Review']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDisplayStep', () => {
|
||||
it('returns actual step for non-script mode', () => {
|
||||
expect(getDisplayStep(1, false)).toBe(1);
|
||||
expect(getDisplayStep(2, false)).toBe(2);
|
||||
expect(getDisplayStep(3, false)).toBe(3);
|
||||
expect(getDisplayStep(4, false)).toBe(4);
|
||||
expect(getDisplayStep(5, false)).toBe(5);
|
||||
expect(getDisplayStep(6, false)).toBe(6);
|
||||
});
|
||||
|
||||
it('maps steps correctly for script mode', () => {
|
||||
// Steps 1 and 2 stay the same
|
||||
expect(getDisplayStep(1, true)).toBe(1);
|
||||
expect(getDisplayStep(2, true)).toBe(2);
|
||||
// Step 5 (Agent Chat) becomes display step 3
|
||||
expect(getDisplayStep(5, true)).toBe(3);
|
||||
// Step 6 (Review) becomes display step 4
|
||||
expect(getDisplayStep(6, true)).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WIZARD_STEPS', () => {
|
||||
it('has correct step numbers', () => {
|
||||
expect(WIZARD_STEPS.BASIC_INFO).toBe(1);
|
||||
expect(WIZARD_STEPS.COMPLEXITY).toBe(2);
|
||||
expect(WIZARD_STEPS.CLIENT_MODE).toBe(3);
|
||||
expect(WIZARD_STEPS.AUTONOMY).toBe(4);
|
||||
expect(WIZARD_STEPS.AGENT_CHAT).toBe(5);
|
||||
expect(WIZARD_STEPS.REVIEW).toBe(6);
|
||||
});
|
||||
});
|
||||
357
frontend/tests/components/projects/wizard/useWizardState.test.ts
Normal file
357
frontend/tests/components/projects/wizard/useWizardState.test.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user