diff --git a/archon-ui-main/.dockerignore b/archon-ui-main/.dockerignore index bbae0365..9e1e2818 100644 --- a/archon-ui-main/.dockerignore +++ b/archon-ui-main/.dockerignore @@ -43,6 +43,16 @@ docker-compose.yml # Tests coverage test-results +tests/ +**/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx +**/__tests__ +**/*.e2e.test.ts +**/*.integration.test.ts +vitest.config.ts +tsconfig.prod.json # Documentation README.md diff --git a/archon-ui-main/__mocks__/lucide-react.tsx b/archon-ui-main/__mocks__/lucide-react.tsx deleted file mode 100644 index a3553fe1..00000000 --- a/archon-ui-main/__mocks__/lucide-react.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import React from 'react' -import { vi } from 'vitest' - -const createMockIcon = (name: string) => { - const MockIcon = React.forwardRef(({ className, ...props }: any, ref: any) => ( - - {name} - - )) - MockIcon.displayName = name - return MockIcon -} - -// Export all icons used in the app -export const Settings = createMockIcon('Settings') -export const Check = createMockIcon('Check') -export const CheckCircle = createMockIcon('CheckCircle') -export const X = createMockIcon('X') -export const XCircle = createMockIcon('XCircle') -export const Eye = createMockIcon('Eye') -export const EyeOff = createMockIcon('EyeOff') -export const Save = createMockIcon('Save') -export const Loader = createMockIcon('Loader') -export const Loader2 = createMockIcon('Loader2') -export const RefreshCw = createMockIcon('RefreshCw') -export const Play = createMockIcon('Play') -export const Pause = createMockIcon('Pause') -export const Square = createMockIcon('Square') -export const FileText = createMockIcon('FileText') -export const Download = createMockIcon('Download') -export const Upload = createMockIcon('Upload') -export const ChevronDown = createMockIcon('ChevronDown') -export const ChevronUp = createMockIcon('ChevronUp') -export const ChevronLeft = createMockIcon('ChevronLeft') -export const ChevronRight = createMockIcon('ChevronRight') -export const Plus = createMockIcon('Plus') -export const Minus = createMockIcon('Minus') -export const Edit = createMockIcon('Edit') -export const Edit2 = createMockIcon('Edit2') -export const Edit3 = createMockIcon('Edit3') -export const Trash = createMockIcon('Trash') -export const Trash2 = createMockIcon('Trash2') -export const User = createMockIcon('User') -export const Users = createMockIcon('Users') -export const Bot = createMockIcon('Bot') -export const Database = createMockIcon('Database') -export const Server = createMockIcon('Server') -export const Globe = createMockIcon('Globe') -export const Search = createMockIcon('Search') -export const Filter = createMockIcon('Filter') -export const Copy = createMockIcon('Copy') -export const ExternalLink = createMockIcon('ExternalLink') -export const Info = createMockIcon('Info') -export const AlertCircle = createMockIcon('AlertCircle') -export const AlertTriangle = createMockIcon('AlertTriangle') -export const Zap = createMockIcon('Zap') -export const Code = createMockIcon('Code') -export const Terminal = createMockIcon('Terminal') -export const Book = createMockIcon('Book') -export const BookOpen = createMockIcon('BookOpen') -export const Folder = createMockIcon('Folder') -export const FolderOpen = createMockIcon('FolderOpen') -export const File = createMockIcon('File') -export const Hash = createMockIcon('Hash') -export const Tag = createMockIcon('Tag') -export const Clock = createMockIcon('Clock') -export const Calendar = createMockIcon('Calendar') -export const MapPin = createMockIcon('MapPin') -export const Link = createMockIcon('Link') -export const Mail = createMockIcon('Mail') -export const Phone = createMockIcon('Phone') -export const Home = createMockIcon('Home') -export const Menu = createMockIcon('Menu') -export const MoreHorizontal = createMockIcon('MoreHorizontal') -export const MoreVertical = createMockIcon('MoreVertical') -export const Refresh = createMockIcon('Refresh') -export const RotateCcw = createMockIcon('RotateCcw') -export const RotateCw = createMockIcon('RotateCw') -export const Sun = createMockIcon('Sun') -export const Moon = createMockIcon('Moon') -export const Monitor = createMockIcon('Monitor') -export const Wifi = createMockIcon('Wifi') -export const WifiOff = createMockIcon('WifiOff') -export const Volume2 = createMockIcon('Volume2') -export const VolumeX = createMockIcon('VolumeX') -export const BarChart = createMockIcon('BarChart') -export const PieChart = createMockIcon('PieChart') -export const TrendingUp = createMockIcon('TrendingUp') -export const TrendingDown = createMockIcon('TrendingDown') -export const ArrowUp = createMockIcon('ArrowUp') -export const ArrowDown = createMockIcon('ArrowDown') -export const ArrowLeft = createMockIcon('ArrowLeft') -export const ArrowRight = createMockIcon('ArrowRight') -export const Send = createMockIcon('Send') -export const MessageSquare = createMockIcon('MessageSquare') -export const MessageCircle = createMockIcon('MessageCircle') -export const Heart = createMockIcon('Heart') -export const Star = createMockIcon('Star') -export const Bookmark = createMockIcon('Bookmark') -export const Share = createMockIcon('Share') -export const Share2 = createMockIcon('Share2') -export const Maximize = createMockIcon('Maximize') -export const Minimize = createMockIcon('Minimize') -export const Expand = createMockIcon('Expand') -export const Shrink = createMockIcon('Shrink') -export const Move = createMockIcon('Move') -export const Shuffle = createMockIcon('Shuffle') -export const Repeat = createMockIcon('Repeat') -export const StopCircle = createMockIcon('StopCircle') -export const SkipBack = createMockIcon('SkipBack') -export const SkipForward = createMockIcon('SkipForward') -export const FastForward = createMockIcon('FastForward') -export const Rewind = createMockIcon('Rewind') -export const Camera = createMockIcon('Camera') -export const Image = createMockIcon('Image') -export const Video = createMockIcon('Video') -export const Mic = createMockIcon('Mic') -export const MicOff = createMockIcon('MicOff') -export const Headphones = createMockIcon('Headphones') -export const Speaker = createMockIcon('Speaker') -export const Bell = createMockIcon('Bell') -export const BellOff = createMockIcon('BellOff') -export const Shield = createMockIcon('Shield') -export const ShieldCheck = createMockIcon('ShieldCheck') -export const ShieldAlert = createMockIcon('ShieldAlert') -export const Key = createMockIcon('Key') -export const Lock = createMockIcon('Lock') -export const Unlock = createMockIcon('Unlock') -export const LogIn = createMockIcon('LogIn') -export const LogOut = createMockIcon('LogOut') -export const UserPlus = createMockIcon('UserPlus') -export const UserMinus = createMockIcon('UserMinus') -export const UserCheck = createMockIcon('UserCheck') -export const UserX = createMockIcon('UserX') -export const Package = createMockIcon('Package') -export const Package2 = createMockIcon('Package2') -export const ShoppingCart = createMockIcon('ShoppingCart') -export const ShoppingBag = createMockIcon('ShoppingBag') -export const CreditCard = createMockIcon('CreditCard') -export const DollarSign = createMockIcon('DollarSign') -export const Percent = createMockIcon('Percent') -export const Activity = createMockIcon('Activity') -export const Cpu = createMockIcon('Cpu') -export const HardDrive = createMockIcon('HardDrive') -export const MemoryStick = createMockIcon('MemoryStick') -export const Smartphone = createMockIcon('Smartphone') -export const Tablet = createMockIcon('Tablet') -export const Laptop = createMockIcon('Laptop') -export const Monitor2 = createMockIcon('Monitor2') -export const Tv = createMockIcon('Tv') -export const Watch = createMockIcon('Watch') -export const Gamepad2 = createMockIcon('Gamepad2') -export const Mouse = createMockIcon('Mouse') -export const Keyboard = createMockIcon('Keyboard') -export const Printer = createMockIcon('Printer') -export const Scanner = createMockIcon('Scanner') -export const Webcam = createMockIcon('Webcam') -export const Bluetooth = createMockIcon('Bluetooth') -export const Usb = createMockIcon('Usb') -export const Zap2 = createMockIcon('Zap2') -export const Battery = createMockIcon('Battery') -export const BatteryCharging = createMockIcon('BatteryCharging') -export const Plug = createMockIcon('Plug') -export const Power = createMockIcon('Power') -export const PowerOff = createMockIcon('PowerOff') -export const BarChart2 = createMockIcon('BarChart2') -export const BarChart3 = createMockIcon('BarChart3') -export const BarChart4 = createMockIcon('BarChart4') -export const LineChart = createMockIcon('LineChart') -export const PieChart2 = createMockIcon('PieChart2') -export const Layers = createMockIcon('Layers') -export const Layers2 = createMockIcon('Layers2') -export const Layers3 = createMockIcon('Layers3') -export const Grid = createMockIcon('Grid') -export const Grid2x2 = createMockIcon('Grid2x2') -export const Grid3x3 = createMockIcon('Grid3x3') -export const List = createMockIcon('List') -export const ListChecks = createMockIcon('ListChecks') -export const ListTodo = createMockIcon('ListTodo') -export const CheckSquare = createMockIcon('CheckSquare') -export const Square2 = createMockIcon('Square2') -export const Circle = createMockIcon('Circle') -export const CircleCheck = createMockIcon('CircleCheck') -export const CircleX = createMockIcon('CircleX') -export const CircleDot = createMockIcon('CircleDot') -export const Target = createMockIcon('Target') -export const Focus = createMockIcon('Focus') -export const Crosshair = createMockIcon('Crosshair') -export const Locate = createMockIcon('Locate') -export const LocateFixed = createMockIcon('LocateFixed') -export const Navigation = createMockIcon('Navigation') -export const Navigation2 = createMockIcon('Navigation2') -export const Compass = createMockIcon('Compass') -export const Map = createMockIcon('Map') -export const TestTube = createMockIcon('TestTube') -export const FlaskConical = createMockIcon('FlaskConical') -export const Bug = createMockIcon('Bug') -export const GitBranch = createMockIcon('GitBranch') -export const GitCommit = createMockIcon('GitCommit') -export const GitMerge = createMockIcon('GitMerge') -export const GitPullRequest = createMockIcon('GitPullRequest') -export const Github = createMockIcon('Github') -export const Gitlab = createMockIcon('Gitlab') -export const Bitbucket = createMockIcon('Bitbucket') -export const Network = createMockIcon('Network') -export const GitGraph = createMockIcon('GitGraph') -export const ListFilter = createMockIcon('ListFilter') -export const CheckSquare2 = createMockIcon('CheckSquare2') -export const CircleSlash2 = createMockIcon('CircleSlash2') -export const Clock3 = createMockIcon('Clock3') -export const GitCommitHorizontal = createMockIcon('GitCommitHorizontal') -export const CalendarDays = createMockIcon('CalendarDays') -export const Sparkles = createMockIcon('Sparkles') -export const Layout = createMockIcon('Layout') -export const Table = createMockIcon('Table') -export const Columns = createMockIcon('Columns') -export const GitPullRequestDraft = createMockIcon('GitPullRequestDraft') -export const BrainCircuit = createMockIcon('BrainCircuit') -export const Wrench = createMockIcon('Wrench') -export const PlugZap = createMockIcon('PlugZap') -export const BoxIcon = createMockIcon('BoxIcon') -export const Box = createMockIcon('Box') -export const Brain = createMockIcon('Brain') -export const LinkIcon = createMockIcon('LinkIcon') -export const Sparkle = createMockIcon('Sparkle') -export const FolderTree = createMockIcon('FolderTree') -export const Lightbulb = createMockIcon('Lightbulb') -export const Rocket = createMockIcon('Rocket') -export const Building = createMockIcon('Building') -export const FileCode = createMockIcon('FileCode') -export const FileJson = createMockIcon('FileJson') -export const Braces = createMockIcon('Braces') -export const Binary = createMockIcon('Binary') -export const Palette = createMockIcon('Palette') -export const Paintbrush = createMockIcon('Paintbrush') -export const Type = createMockIcon('Type') -export const Heading = createMockIcon('Heading') -export const AlignLeft = createMockIcon('AlignLeft') -export const AlignCenter = createMockIcon('AlignCenter') -export const AlignRight = createMockIcon('AlignRight') -export const Bold = createMockIcon('Bold') -export const Italic = createMockIcon('Italic') -export const Underline = createMockIcon('Underline') -export const Strikethrough = createMockIcon('Strikethrough') -export const FileCheck = createMockIcon('FileCheck') -export const FileX = createMockIcon('FileX') -export const FilePlus = createMockIcon('FilePlus') -export const FileMinus = createMockIcon('FileMinus') -export const FolderPlus = createMockIcon('FolderPlus') -export const FolderMinus = createMockIcon('FolderMinus') -export const FolderCheck = createMockIcon('FolderCheck') -export const FolderX = createMockIcon('FolderX') -export const startMCPServer = createMockIcon('startMCPServer') -export const Pin = createMockIcon('Pin') -export const CheckCircle2 = createMockIcon('CheckCircle2') -export const Clipboard = createMockIcon('Clipboard') -export const LayoutGrid = createMockIcon('LayoutGrid') -export const Pencil = createMockIcon('Pencil') -export const MousePointer = createMockIcon('MousePointer') -export const GripVertical = createMockIcon('GripVertical') -export const History = createMockIcon('History') -export const PlusCircle = createMockIcon('PlusCircle') -export const MinusCircle = createMockIcon('MinusCircle') -export const ChevronDownIcon = createMockIcon('ChevronDownIcon') -export const FileIcon = createMockIcon('FileIcon') -export const AlertCircleIcon = createMockIcon('AlertCircleIcon') -export const Clock4 = createMockIcon('Clock4') -export const XIcon = createMockIcon('XIcon') -export const CheckIcon = createMockIcon('CheckIcon') -export const TrashIcon = createMockIcon('TrashIcon') -export const EyeIcon = createMockIcon('EyeIcon') -export const EditIcon = createMockIcon('EditIcon') -export const DownloadIcon = createMockIcon('DownloadIcon') -export const RefreshIcon = createMockIcon('RefreshIcon') -export const SearchIcon = createMockIcon('SearchIcon') -export const FilterIcon = createMockIcon('FilterIcon') -export const PlusIcon = createMockIcon('PlusIcon') -export const FolderIcon = createMockIcon('FolderIcon') -export const FileTextIcon = createMockIcon('FileTextIcon') -export const BookOpenIcon = createMockIcon('BookOpenIcon') -export const DatabaseIcon = createMockIcon('DatabaseIcon') -export const GlobeIcon = createMockIcon('GlobeIcon') -export const TagIcon = createMockIcon('TagIcon') -export const CalendarIcon = createMockIcon('CalendarIcon') -export const ClockIcon = createMockIcon('ClockIcon') -export const UserIcon = createMockIcon('UserIcon') -export const SettingsIcon = createMockIcon('SettingsIcon') -export const InfoIcon = createMockIcon('InfoIcon') -export const WarningIcon = createMockIcon('WarningIcon') -export const ErrorIcon = createMockIcon('ErrorIcon') \ No newline at end of file diff --git a/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx new file mode 100644 index 00000000..e119c749 --- /dev/null +++ b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx @@ -0,0 +1,189 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '../../../testing/test-utils'; +import { ProjectCard } from '../ProjectCard'; +import type { Project } from '../../types'; + +describe('ProjectCard', () => { + const mockProject: Project = { + id: 'project-1', + title: 'Test Project', + description: 'Test Description', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + pinned: false, + features: [], + docs: [], + }; + + const mockTaskCounts = { + todo: 5, + doing: 3, + review: 2, + done: 10, + }; + + const mockHandlers = { + onSelect: vi.fn(), + onPin: vi.fn(), + onDelete: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render project title', () => { + render( + + ); + + expect(screen.getByText('Test Project')).toBeInTheDocument(); + }); + + it('should display task counts', () => { + render( + + ); + + // Task count badges should be visible + // Note: Component only shows todo, doing, and done (not review) + const fives = screen.getAllByText('5'); + expect(fives.length).toBeGreaterThan(0); // todo count + expect(screen.getByText('10')).toBeInTheDocument(); // done + // Doing count might be displayed as 3 or duplicated - implementation detail + }); + + it('should call onSelect when clicked', () => { + render( + + ); + + const card = screen.getByRole('listitem'); + fireEvent.click(card); + + expect(mockHandlers.onSelect).toHaveBeenCalledWith(mockProject); + expect(mockHandlers.onSelect).toHaveBeenCalledTimes(1); + }); + + it('should apply selected styles when isSelected is true', () => { + const { container } = render( + + ); + + const card = container.querySelector('[role="listitem"]'); + // Check for selected-specific classes + expect(card?.className).toContain('scale-[1.02]'); + expect(card?.className).toContain('border-purple'); + }); + + it('should apply pinned styles when project is pinned', () => { + const pinnedProject = { ...mockProject, pinned: true }; + + const { container } = render( + + ); + + const card = container.querySelector('[role="listitem"]'); + // Check for pinned-specific classes + expect(card?.className).toContain('from-purple'); + expect(card?.className).toContain('border-purple-500'); + }); + + it('should render aurora glow effect when selected', () => { + const { container } = render( + + ); + + // Aurora glow div should exist when selected + const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]'); + expect(glowEffect).toBeInTheDocument(); + }); + + it('should not render aurora glow effect when not selected', () => { + const { container } = render( + + ); + + // Aurora glow div should not exist when not selected + const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]'); + expect(glowEffect).not.toBeInTheDocument(); + }); + + it('should show zero task counts correctly', () => { + const zeroTaskCounts = { + todo: 0, + doing: 0, + review: 0, + done: 0, + }; + + render( + + ); + + // All counts should show 0 (ProjectCard may not show review count) + const zeros = screen.getAllByText('0'); + expect(zeros.length).toBeGreaterThanOrEqual(3); // At least todo, doing, done + }); + + it('should handle very long project titles', () => { + const longTitleProject = { + ...mockProject, + title: 'This is an extremely long project title that should be truncated properly to avoid breaking the layout of the card component', + }; + + render( + + ); + + const title = screen.getByText(/This is an extremely long project title/); + expect(title).toBeInTheDocument(); + // Title should have line-clamp-2 class + expect(title.className).toContain('line-clamp-2'); + }); +}); \ No newline at end of file diff --git a/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts new file mode 100644 index 00000000..0b90ba95 --- /dev/null +++ b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { projectKeys, useProjects, useCreateProject, useUpdateProject, useDeleteProject } from '../useProjectQueries'; +import type { Project } from '../../types'; +import React from 'react'; + +// Mock the services +vi.mock('../../services', () => ({ + projectService: { + listProjects: vi.fn(), + createProject: vi.fn(), + updateProject: vi.fn(), + deleteProject: vi.fn(), + getProjectFeatures: vi.fn(), + }, + taskService: { + getTaskCountsForAllProjects: vi.fn(), + }, +})); + +// Mock the toast hook +vi.mock('../../../ui/hooks/useToast', () => ({ + useToast: () => ({ + showToast: vi.fn(), + }), +})); + +// Mock smart polling +vi.mock('../../../ui/hooks', () => ({ + useSmartPolling: () => ({ + refetchInterval: 5000, + isPaused: false, + }), +})); + +// Test wrapper with QueryClient +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +}; + +describe('useProjectQueries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('projectKeys', () => { + it('should generate correct query keys', () => { + expect(projectKeys.all).toEqual(['projects']); + expect(projectKeys.lists()).toEqual(['projects', 'list']); + expect(projectKeys.detail('123')).toEqual(['projects', 'detail', '123']); + expect(projectKeys.tasks('123')).toEqual(['projects', 'detail', '123', 'tasks']); + expect(projectKeys.features('123')).toEqual(['projects', 'detail', '123', 'features']); + expect(projectKeys.documents('123')).toEqual(['projects', 'detail', '123', 'documents']); + }); + }); + + describe('useProjects', () => { + it('should fetch projects list', async () => { + const mockProjects: Project[] = [ + { + id: '1', + title: 'Test Project', + description: 'Test Description', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + pinned: false, + features: [], + docs: [], + }, + ]; + + const { projectService } = await import('../../services'); + vi.mocked(projectService.listProjects).mockResolvedValue(mockProjects); + + const { result } = renderHook(() => useProjects(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toEqual(mockProjects); + }); + + expect(projectService.listProjects).toHaveBeenCalledTimes(1); + }); + }); + + describe('useCreateProject', () => { + it('should optimistically add project and replace with server response', async () => { + const newProject: Project = { + id: 'real-id', + title: 'New Project', + description: 'New Description', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + pinned: false, + features: [], + docs: [], + }; + + const { projectService } = await import('../../services'); + vi.mocked(projectService.createProject).mockResolvedValue({ + project: newProject, + message: 'Created', + }); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useCreateProject(), { wrapper }); + + await result.current.mutateAsync({ + title: 'New Project', + description: 'New Description', + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(projectService.createProject).toHaveBeenCalledWith({ + title: 'New Project', + description: 'New Description', + }); + }); + }); + + it('should rollback on error', async () => { + const { projectService } = await import('../../services'); + vi.mocked(projectService.createProject).mockRejectedValue(new Error('Network error')); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useCreateProject(), { wrapper }); + + await expect( + result.current.mutateAsync({ + title: 'New Project', + description: 'New Description', + }) + ).rejects.toThrow('Network error'); + }); + }); + + describe('useUpdateProject', () => { + it('should handle pinning a project', async () => { + const updatedProject: Project = { + id: '1', + title: 'Test Project', + description: 'Test Description', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + pinned: true, + features: [], + docs: [], + }; + + const { projectService } = await import('../../services'); + vi.mocked(projectService.updateProject).mockResolvedValue(updatedProject); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useUpdateProject(), { wrapper }); + + await result.current.mutateAsync({ + projectId: '1', + updates: { pinned: true }, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(projectService.updateProject).toHaveBeenCalledWith('1', { pinned: true }); + }); + }); + }); + + describe('useDeleteProject', () => { + it('should optimistically remove project', async () => { + const { projectService } = await import('../../services'); + vi.mocked(projectService.deleteProject).mockResolvedValue(undefined); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useDeleteProject(), { wrapper }); + + await result.current.mutateAsync('project-to-delete'); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(projectService.deleteProject).toHaveBeenCalledWith('project-to-delete'); + }); + }); + + it('should rollback on delete error', async () => { + const { projectService } = await import('../../services'); + vi.mocked(projectService.deleteProject).mockRejectedValue(new Error('Permission denied')); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useDeleteProject(), { wrapper }); + + await expect( + result.current.mutateAsync('project-to-delete') + ).rejects.toThrow('Permission denied'); + }); + }); +}); \ No newline at end of file diff --git a/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts new file mode 100644 index 00000000..a9282987 --- /dev/null +++ b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { taskKeys, useProjectTasks, useCreateTask } from '../useTaskQueries'; +import type { Task } from '../../types'; +import React from 'react'; + +// Mock the services +vi.mock('../../services', () => ({ + taskService: { + getTasksByProject: vi.fn(), + createTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + }, +})); + +// Mock the toast hook +vi.mock('../../../../ui/hooks/useToast', () => ({ + useToast: () => ({ + showToast: vi.fn(), + }), +})); + +// Mock smart polling +vi.mock('../../../../ui/hooks', () => ({ + useSmartPolling: () => ({ + refetchInterval: 5000, + isPaused: false, + }), +})); + +// Test wrapper with QueryClient +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); +}; + +describe('useTaskQueries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('taskKeys', () => { + it('should generate correct query keys', () => { + expect(taskKeys.all('project-123')).toEqual(['projects', 'project-123', 'tasks']); + }); + }); + + describe('useProjectTasks', () => { + it('should fetch tasks for a project', async () => { + const mockTasks: Task[] = [ + { + id: 'task-1', + project_id: 'project-123', + title: 'Test Task', + description: 'Test Description', + status: 'todo', + assignee: 'User', + task_order: 100, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }, + ]; + + const { taskService } = await import('../../services'); + vi.mocked(taskService.getTasksByProject).mockResolvedValue(mockTasks); + + const { result } = renderHook(() => useProjectTasks('project-123'), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toEqual(mockTasks); + }); + + expect(taskService.getTasksByProject).toHaveBeenCalledWith('project-123'); + }); + + it('should not fetch tasks when projectId is undefined', () => { + const { result } = renderHook(() => useProjectTasks(undefined), { + wrapper: createWrapper(), + }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetching).toBe(false); + expect(result.current.data).toBeUndefined(); + }); + + it('should respect enabled flag', () => { + const { result } = renderHook(() => useProjectTasks('project-123', false), { + wrapper: createWrapper(), + }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetching).toBe(false); + expect(result.current.data).toBeUndefined(); + }); + }); + + describe('useCreateTask', () => { + it('should optimistically add task and replace with server response', async () => { + const newTask: Task = { + id: 'real-task-id', + project_id: 'project-123', + title: 'New Task', + description: 'New Description', + status: 'todo', + assignee: 'User', + task_order: 100, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }; + + const { taskService } = await import('../../services'); + vi.mocked(taskService.createTask).mockResolvedValue(newTask); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useCreateTask(), { wrapper }); + + await result.current.mutateAsync({ + project_id: 'project-123', + title: 'New Task', + description: 'New Description', + status: 'todo', + assignee: 'User', + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(taskService.createTask).toHaveBeenCalledWith({ + project_id: 'project-123', + title: 'New Task', + description: 'New Description', + status: 'todo', + assignee: 'User', + }); + }); + }); + + it('should provide default values for optional fields', async () => { + const newTask: Task = { + id: 'real-task-id', + project_id: 'project-123', + title: 'Minimal Task', + description: '', + status: 'todo', + assignee: 'User', + task_order: 100, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }; + + const { taskService } = await import('../../services'); + vi.mocked(taskService.createTask).mockResolvedValue(newTask); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useCreateTask(), { wrapper }); + + await result.current.mutateAsync({ + project_id: 'project-123', + title: 'Minimal Task', + description: '', + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + }); + + it('should rollback on error', async () => { + const { taskService } = await import('../../services'); + vi.mocked(taskService.createTask).mockRejectedValue(new Error('Network error')); + + const wrapper = createWrapper(); + const { result } = renderHook(() => useCreateTask(), { wrapper }); + + await expect( + result.current.mutateAsync({ + project_id: 'project-123', + title: 'Failed Task', + description: 'This will fail', + }) + ).rejects.toThrow('Network error'); + }); + }); +}); \ No newline at end of file diff --git a/archon-ui-main/src/features/testing/test-utils.tsx b/archon-ui-main/src/features/testing/test-utils.tsx new file mode 100644 index 00000000..fbdb1e5b --- /dev/null +++ b/archon-ui-main/src/features/testing/test-utils.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render as rtlRender } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from '../ui/components/ToastProvider'; +import { TooltipProvider } from '../ui/primitives/tooltip'; + +/** + * Custom render function that wraps components with all necessary providers + * This follows the best practice of having a centralized test render utility + */ +export function renderWithProviders( + ui: React.ReactElement, + { + queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }), + ...renderOptions + } = {} +) { + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); + } + + return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); +} + +// Re-export everything from React Testing Library +export * from '@testing-library/react'; + +// Override the default render with our custom one +export { renderWithProviders as render }; \ No newline at end of file diff --git a/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts new file mode 100644 index 00000000..7c84c40e --- /dev/null +++ b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts @@ -0,0 +1,189 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useSmartPolling } from '../useSmartPolling'; + +describe('useSmartPolling', () => { + beforeEach(() => { + // Reset document visibility state + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + configurable: true, + }); + Object.defineProperty(document, 'hidden', { + value: false, + writable: true, + configurable: true, + }); + // Mock document.hasFocus + document.hasFocus = vi.fn(() => true); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.clearAllMocks(); + }); + + it('should return the base interval when document is visible and focused', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + expect(result.current.refetchInterval).toBe(5000); + expect(result.current.isActive).toBe(true); + expect(result.current.isVisible).toBe(true); + expect(result.current.hasFocus).toBe(true); + }); + + it('should disable polling when document is hidden', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + // Initially should be active + expect(result.current.isActive).toBe(true); + expect(result.current.refetchInterval).toBe(5000); + + // Simulate tab becoming hidden + act(() => { + Object.defineProperty(document, 'hidden', { + value: true, + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + + // Should be disabled (returns false) + expect(result.current.isVisible).toBe(false); + expect(result.current.isActive).toBe(false); + expect(result.current.refetchInterval).toBe(false); + }); + + it('should resume polling when document becomes visible again', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + // Make hidden + act(() => { + Object.defineProperty(document, 'hidden', { + value: true, + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + + expect(result.current.refetchInterval).toBe(false); + + // Make visible again + act(() => { + Object.defineProperty(document, 'hidden', { + value: false, + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + + expect(result.current.isVisible).toBe(true); + expect(result.current.isActive).toBe(true); + expect(result.current.refetchInterval).toBe(5000); + }); + + it('should slow down to 60 seconds when window loses focus', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + // Initially focused + expect(result.current.refetchInterval).toBe(5000); + expect(result.current.hasFocus).toBe(true); + + // Simulate window blur + act(() => { + window.dispatchEvent(new Event('blur')); + }); + + // Should be slowed down to 60 seconds + expect(result.current.hasFocus).toBe(false); + expect(result.current.isActive).toBe(false); + expect(result.current.refetchInterval).toBe(60000); + }); + + it('should resume normal speed when window regains focus', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + // Blur window + act(() => { + window.dispatchEvent(new Event('blur')); + }); + + expect(result.current.refetchInterval).toBe(60000); + + // Focus window again + act(() => { + window.dispatchEvent(new Event('focus')); + }); + + expect(result.current.hasFocus).toBe(true); + expect(result.current.isActive).toBe(true); + expect(result.current.refetchInterval).toBe(5000); + }); + + it('should handle different base intervals', () => { + const { result: result1 } = renderHook(() => useSmartPolling(1000)); + const { result: result2 } = renderHook(() => useSmartPolling(10000)); + + expect(result1.current.refetchInterval).toBe(1000); + expect(result2.current.refetchInterval).toBe(10000); + + // When blurred, both should be 60 seconds + act(() => { + window.dispatchEvent(new Event('blur')); + }); + + expect(result1.current.refetchInterval).toBe(60000); + expect(result2.current.refetchInterval).toBe(60000); + }); + + it('should use default interval of 10000ms when not specified', () => { + const { result } = renderHook(() => useSmartPolling()); + + expect(result.current.refetchInterval).toBe(10000); + }); + + it('should cleanup event listeners on unmount', () => { + const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); + const windowRemoveEventListenerSpy = vi.spyOn(window, 'removeEventListener'); + + const { unmount } = renderHook(() => useSmartPolling(5000)); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function)); + expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); + expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('blur', expect.any(Function)); + + removeEventListenerSpy.mockRestore(); + windowRemoveEventListenerSpy.mockRestore(); + }); + + it('should correctly report isActive state', () => { + const { result } = renderHook(() => useSmartPolling(5000)); + + // Active when both visible and focused + expect(result.current.isActive).toBe(true); + + // Not active when not focused + act(() => { + window.dispatchEvent(new Event('blur')); + }); + expect(result.current.isActive).toBe(false); + + // Not active when hidden + act(() => { + window.dispatchEvent(new Event('focus')); // Focus first + Object.defineProperty(document, 'hidden', { + value: true, + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + expect(result.current.isActive).toBe(false); + }); +}); \ No newline at end of file diff --git a/archon-ui-main/test/components.test.tsx b/archon-ui-main/test/components.test.tsx deleted file mode 100644 index d38d15f5..00000000 --- a/archon-ui-main/test/components.test.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi } from 'vitest' -import React from 'react' - -describe('Component Tests', () => { - test('button component works', () => { - const onClick = vi.fn() - const MockButton = ({ children, ...props }: any) => ( - - ) - - render(Click me) - - const button = screen.getByRole('button') - fireEvent.click(button) - expect(onClick).toHaveBeenCalledTimes(1) - }) - - test('input component works', () => { - const MockInput = () => { - const [value, setValue] = React.useState('') - return ( - setValue(e.target.value)} - placeholder="Test input" - /> - ) - } - - render() - const input = screen.getByPlaceholderText('Test input') - - fireEvent.change(input, { target: { value: 'test' } }) - expect((input as HTMLInputElement).value).toBe('test') - }) - - test('modal component works', () => { - const MockModal = () => { - const [isOpen, setIsOpen] = React.useState(false) - return ( -
- - {isOpen && ( -
-

Modal Title

- -
- )} -
- ) - } - - render() - - // Modal not visible initially - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - - // Open modal - fireEvent.click(screen.getByText('Open Modal')) - expect(screen.getByRole('dialog')).toBeInTheDocument() - - // Close modal - fireEvent.click(screen.getByText('Close')) - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - }) - - test('progress bar component works', () => { - const MockProgressBar = ({ value, max }: { value: number; max: number }) => ( -
-
Progress: {Math.round((value / max) * 100)}%
-
Bar
-
- ) - - const { rerender } = render() - expect(screen.getByText('Progress: 0%')).toBeInTheDocument() - - rerender() - expect(screen.getByText('Progress: 50%')).toBeInTheDocument() - - rerender() - expect(screen.getByText('Progress: 100%')).toBeInTheDocument() - }) - - test('tooltip component works', () => { - const MockTooltip = ({ children, tooltip }: any) => { - const [show, setShow] = React.useState(false) - return ( -
- - {show &&
{tooltip}
} -
- ) - } - - render(Hover me) - - const button = screen.getByText('Hover me') - expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() - - fireEvent.mouseEnter(button) - expect(screen.getByRole('tooltip')).toBeInTheDocument() - - fireEvent.mouseLeave(button) - expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() - }) - - test('accordion component works', () => { - const MockAccordion = () => { - const [expanded, setExpanded] = React.useState(false) - return ( -
- - {expanded &&
Section content
} -
- ) - } - - render() - - expect(screen.queryByText('Section content')).not.toBeInTheDocument() - - fireEvent.click(screen.getByText('Section 1 +')) - expect(screen.getByText('Section content')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Section 1 −')) - expect(screen.queryByText('Section content')).not.toBeInTheDocument() - }) - - test('table sorting works', () => { - const MockTable = () => { - const [data, setData] = React.useState([ - { name: 'Alice', age: 30 }, - { name: 'Bob', age: 25 }, - { name: 'Charlie', age: 35 } - ]) - - const sortByName = () => { - setData([...data].sort((a, b) => a.name.localeCompare(b.name))) - } - - return ( - - - - - - - - - {data.map((row, index) => ( - - - - - ))} - -
- Name - Age
{row.name}{row.age}
- ) - } - - render() - - const cells = screen.getAllByRole('cell') - expect(cells[0]).toHaveTextContent('Alice') - - fireEvent.click(screen.getByText('Name')) - - // After sorting, Alice should still be first (already sorted) - const sortedCells = screen.getAllByRole('cell') - expect(sortedCells[0]).toHaveTextContent('Alice') - }) - - test('pagination works', () => { - const MockPagination = () => { - const [page, setPage] = React.useState(1) - return ( -
-
Page {page}
- - -
- ) - } - - render() - - expect(screen.getByText('Page 1')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Next')) - expect(screen.getByText('Page 2')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Previous')) - expect(screen.getByText('Page 1')).toBeInTheDocument() - }) - - test('form validation works', () => { - const MockForm = () => { - const [email, setEmail] = React.useState('') - const [error, setError] = React.useState('') - - const validate = (value: string) => { - if (!value) { - setError('Email is required') - } else if (!value.includes('@')) { - setError('Invalid email format') - } else { - setError('') - } - } - - return ( -
- { - setEmail(e.target.value) - validate(e.target.value) - }} - /> - {error &&
{error}
} -
- ) - } - - render() - - const input = screen.getByPlaceholderText('Email') - - fireEvent.change(input, { target: { value: 'invalid' } }) - expect(screen.getByRole('alert')).toHaveTextContent('Invalid email format') - - fireEvent.change(input, { target: { value: 'valid@email.com' } }) - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) - - test('search filtering works', () => { - const MockSearch = () => { - const [query, setQuery] = React.useState('') - const items = ['Apple', 'Banana', 'Cherry', 'Date'] - const filtered = items.filter(item => - item.toLowerCase().includes(query.toLowerCase()) - ) - - return ( -
- setQuery(e.target.value)} - /> -
    - {filtered.map((item, index) => ( -
  • {item}
  • - ))} -
-
- ) - } - - render() - - // All items visible initially - expect(screen.getByText('Apple')).toBeInTheDocument() - expect(screen.getByText('Banana')).toBeInTheDocument() - - // Filter items - const input = screen.getByPlaceholderText('Search items') - fireEvent.change(input, { target: { value: 'a' } }) - - expect(screen.getByText('Apple')).toBeInTheDocument() - expect(screen.getByText('Banana')).toBeInTheDocument() - expect(screen.queryByText('Cherry')).not.toBeInTheDocument() - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx b/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx deleted file mode 100644 index cef0203e..00000000 --- a/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import { DeleteConfirmModal } from '../../../src/components/common/DeleteConfirmModal'; - -describe('DeleteConfirmModal', () => { - const defaultProps = { - itemName: 'Test Item', - onConfirm: vi.fn(), - onCancel: vi.fn(), - type: 'task' as const, - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('renders with correct title and message for task type', () => { - render(); - - expect(screen.getByText('Delete Task')).toBeInTheDocument(); - expect(screen.getByText(/Are you sure you want to delete the "Test Item" task/)).toBeInTheDocument(); - }); - - it('renders with correct title and message for project type', () => { - render(); - - expect(screen.getByText('Delete Project')).toBeInTheDocument(); - expect(screen.getByText(/Are you sure you want to delete the "Test Item" project/)).toBeInTheDocument(); - }); - - it('renders with correct title and message for client type', () => { - render(); - - expect(screen.getByText('Delete MCP Client')).toBeInTheDocument(); - expect(screen.getByText(/Are you sure you want to delete the "Test Item" client/)).toBeInTheDocument(); - }); - - it('calls onConfirm when Delete button is clicked', () => { - render(); - - fireEvent.click(screen.getByText('Delete')); - - expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1); - }); - - it('calls onCancel when Cancel button is clicked', () => { - render(); - - fireEvent.click(screen.getByText('Cancel')); - - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); - }); - - it('calls onCancel when Escape key is pressed', () => { - render(); - - fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' }); - - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); - }); - - it('calls onCancel when backdrop is clicked', () => { - render(); - - // Click the backdrop - const backdrop = screen.getByTestId('modal-backdrop'); - fireEvent.click(backdrop); - - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); - }); - - it('does not call onCancel when modal content is clicked', () => { - render(); - - // Click the modal dialog itself - fireEvent.click(screen.getByRole('dialog')); - - expect(defaultProps.onCancel).not.toHaveBeenCalled(); - }); - - it('has proper accessibility attributes', () => { - render(); - - const dialog = screen.getByRole('dialog'); - expect(dialog).toHaveAttribute('aria-modal', 'true'); - expect(dialog).toHaveAttribute('aria-labelledby'); - expect(dialog).toHaveAttribute('aria-describedby'); - }); - - it('focuses Cancel button by default', () => { - render(); - - const cancelButton = screen.getByText('Cancel'); - expect(cancelButton).toHaveFocus(); - }); - - it('has proper button types', () => { - render(); - - const cancelButton = screen.getByText('Cancel'); - const deleteButton = screen.getByText('Delete'); - - expect(cancelButton).toHaveAttribute('type', 'button'); - expect(deleteButton).toHaveAttribute('type', 'button'); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx b/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx deleted file mode 100644 index 64cb4f8b..00000000 --- a/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx +++ /dev/null @@ -1,407 +0,0 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import { describe, test, expect, vi, beforeEach } from 'vitest' -import React from 'react' - -// Mock the dependencies -vi.mock('../../../src/contexts/ToastContext', () => ({ - useToast: () => ({ - showToast: vi.fn() - }) -})) - -vi.mock('../../../src/services/projectService', () => ({ - projectService: { - getProjectDocuments: vi.fn().mockResolvedValue([]), - deleteDocument: vi.fn().mockResolvedValue(undefined), - updateDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Updated' }), - getDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Document 1' }) - } -})) - -vi.mock('../../../src/services/knowledgeBaseService', () => ({ - knowledgeBaseService: { - getItems: vi.fn().mockResolvedValue([]) - } -})) - -// Create a minimal DocsTab component for testing -const DocsTabTest = () => { - const [documents, setDocuments] = React.useState([ - { - id: 'doc-1', - title: 'Document 1', - content: { type: 'prp' }, - document_type: 'prp', - updated_at: '2025-07-30T12:00:00Z' - }, - { - id: 'doc-2', - title: 'Document 2', - content: { type: 'technical' }, - document_type: 'technical', - updated_at: '2025-07-30T13:00:00Z' - }, - { - id: 'doc-3', - title: 'Document 3', - content: { type: 'business' }, - document_type: 'business', - updated_at: '2025-07-30T14:00:00Z' - } - ]) - - const [selectedDocument, setSelectedDocument] = React.useState(documents[0]) - const { showToast } = { showToast: vi.fn() } - - return ( -
-
- {documents.map(doc => ( -
setSelectedDocument(doc)} - > -
{doc.document_type}
-

{doc.title}

- {selectedDocument?.id !== doc.id && ( - - )} -
- ))} -
console.log('New document')} - > - New Document -
-
- {selectedDocument && ( -
- Selected: {selectedDocument.title} -
- )} -
- ) -} - -describe('DocsTab Document Cards Integration', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - test('renders all document cards', () => { - render() - - expect(screen.getByTestId('document-card-doc-1')).toBeInTheDocument() - expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument() - expect(screen.getByTestId('document-card-doc-3')).toBeInTheDocument() - expect(screen.getByTestId('new-document-card')).toBeInTheDocument() - }) - - test('shows active state on selected document', () => { - render() - - const doc1 = screen.getByTestId('document-card-doc-1') - expect(doc1.className).toContain('border-blue-500') - - const doc2 = screen.getByTestId('document-card-doc-2') - expect(doc2.className).not.toContain('border-blue-500') - }) - - test('switches between documents', () => { - render() - - // Initially doc-1 is selected - expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1') - - // Click on doc-2 - fireEvent.click(screen.getByTestId('document-card-doc-2')) - - // Now doc-2 should be selected - expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2') - - // Check active states - expect(screen.getByTestId('document-card-doc-1').className).not.toContain('border-blue-500') - expect(screen.getByTestId('document-card-doc-2').className).toContain('border-blue-500') - }) - - test('deletes document with confirmation', () => { - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - - render() - - // Click delete on doc-2 - const deleteButton = screen.getByTestId('delete-doc-2') - fireEvent.click(deleteButton) - - expect(confirmSpy).toHaveBeenCalledWith('Delete "Document 2"?') - - // Document should be removed - expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument() - - confirmSpy.mockRestore() - }) - - test('cancels delete when user declines', () => { - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) - - render() - - // Click delete on doc-2 - const deleteButton = screen.getByTestId('delete-doc-2') - fireEvent.click(deleteButton) - - // Document should still be there - expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument() - - confirmSpy.mockRestore() - }) - - test('selects next document when deleting active document', () => { - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - - render() - - // doc-1 is initially selected - expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1') - - // Switch to doc-2 - fireEvent.click(screen.getByTestId('document-card-doc-2')) - expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2') - - // Switch to doc-1 to delete a non-selected document - fireEvent.click(screen.getByTestId('document-card-doc-1')) - - // Delete doc-2 (not currently selected - it should have delete button) - const deleteButton = screen.getByTestId('delete-doc-2') - fireEvent.click(deleteButton) - - // Should automatically select another document - expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document') - expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument() - - confirmSpy.mockRestore() - }) - - test('does not show delete button on active card', () => { - render() - - // doc-1 is active, should not have delete button - expect(screen.queryByTestId('delete-doc-1')).not.toBeInTheDocument() - - // doc-2 is not active, should have delete button - expect(screen.getByTestId('delete-doc-2')).toBeInTheDocument() - }) - - test('horizontal scroll container has correct classes', () => { - const { container } = render() - - const scrollContainer = container.querySelector('.overflow-x-auto') - expect(scrollContainer).toBeInTheDocument() - expect(scrollContainer?.className).toContain('scrollbar-thin') - expect(scrollContainer?.className).toContain('scrollbar-thumb-gray-300') - }) - - test('document cards maintain fixed width', () => { - render() - - const cards = screen.getAllByTestId(/document-card-doc-/) - cards.forEach(card => { - expect(card.className).toContain('flex-shrink-0') - expect(card.className).toContain('w-48') - }) - }) -}) - -describe('DocsTab Document API Integration', () => { - test('calls deleteDocument API when deleting a document', async () => { - const { projectService } = await import('../../../src/services/projectService') - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - - // Create a test component that uses the actual deletion logic - const DocsTabWithAPI = () => { - const [documents, setDocuments] = React.useState([ - { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' }, - { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' } - ]) - const [selectedDocument, setSelectedDocument] = React.useState(documents[0]) - const project = { id: 'proj-123', title: 'Test Project' } - const { showToast } = { showToast: vi.fn() } - - const handleDelete = async (docId: string) => { - try { - // This mirrors the actual DocsTab deletion logic - await projectService.deleteDocument(project.id, docId) - setDocuments(prev => prev.filter(d => d.id !== docId)) - if (selectedDocument?.id === docId) { - setSelectedDocument(documents.find(d => d.id !== docId) || null) - } - showToast('Document deleted', 'success') - } catch (error) { - console.error('Failed to delete document:', error) - showToast('Failed to delete document', 'error') - } - } - - return ( -
- {documents.map(doc => ( -
- {doc.title} - -
- ))} -
- ) - } - - render() - - // Click delete button - fireEvent.click(screen.getByTestId('delete-doc-2')) - - // Wait for async operations - await waitFor(() => { - expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-2') - }) - - // Verify document is removed from UI - expect(screen.queryByTestId('doc-doc-2')).not.toBeInTheDocument() - - confirmSpy.mockRestore() - }) - - test('handles deletion API errors gracefully', async () => { - const { projectService } = await import('../../../src/services/projectService') - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - // Make deleteDocument reject - projectService.deleteDocument = vi.fn().mockRejectedValue(new Error('API Error')) - - const DocsTabWithError = () => { - const [documents, setDocuments] = React.useState([ - { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' } - ]) - const project = { id: 'proj-123', title: 'Test Project' } - const showToast = vi.fn() - - const handleDelete = async (docId: string) => { - try { - await projectService.deleteDocument(project.id, docId) - setDocuments(prev => prev.filter(d => d.id !== docId)) - showToast('Document deleted', 'success') - } catch (error) { - console.error('Failed to delete document:', error) - showToast('Failed to delete document', 'error') - } - } - - return ( -
- {documents.map(doc => ( -
- -
- ))} -
-
- ) - } - - render() - - // Click delete button - fireEvent.click(screen.getByTestId('delete-doc-1')) - - // Wait for async operations - await waitFor(() => { - expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-1') - }) - - // Document should still be in UI due to error - expect(screen.getByTestId('doc-doc-1')).toBeInTheDocument() - - // Error should be logged - expect(consoleSpy).toHaveBeenCalledWith('Failed to delete document:', expect.any(Error)) - - confirmSpy.mockRestore() - consoleSpy.mockRestore() - }) - - test('deletion persists after page refresh', async () => { - const { projectService } = await import('../../../src/services/projectService') - - // Simulate documents before deletion - let mockDocuments = [ - { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' }, - { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' } - ] - - // First render - before deletion - const { rerender } = render(
{mockDocuments.length}
) - expect(screen.getByTestId('docs-count')).toHaveTextContent('2') - - // Mock deleteDocument to also update the mock data - projectService.deleteDocument = vi.fn().mockImplementation(async (projectId, docId) => { - mockDocuments = mockDocuments.filter(d => d.id !== docId) - return Promise.resolve() - }) - - // Mock the list function to return current state - projectService.listProjectDocuments = vi.fn().mockImplementation(async () => { - return mockDocuments - }) - - // Perform deletion - await projectService.deleteDocument('proj-123', 'doc-2') - - // Simulate page refresh by re-fetching documents - const refreshedDocs = await projectService.listProjectDocuments('proj-123') - - // Re-render with refreshed data - rerender(
{refreshedDocs.length}
) - - // Should only have 1 document after refresh - expect(screen.getByTestId('docs-count')).toHaveTextContent('1') - expect(refreshedDocs).toHaveLength(1) - expect(refreshedDocs[0].id).toBe('doc-1') - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx b/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx deleted file mode 100644 index 08a4906b..00000000 --- a/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi } from 'vitest' -import React from 'react' -import { DocumentCard, NewDocumentCard } from '../../../src/components/project-tasks/DocumentCard' -import type { ProjectDoc } from '../../../src/components/project-tasks/DocumentCard' - -describe('DocumentCard', () => { - const mockDocument: ProjectDoc = { - id: 'doc-1', - title: 'Test Document', - content: { test: 'content' }, - document_type: 'prp', - updated_at: '2025-07-30T12:00:00Z', - } - - const mockHandlers = { - onSelect: vi.fn(), - onDelete: vi.fn(), - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - test('renders document card with correct content', () => { - render( - - ) - - expect(screen.getByText('Test Document')).toBeInTheDocument() - expect(screen.getByText('prp')).toBeInTheDocument() - expect(screen.getByText('7/30/2025')).toBeInTheDocument() - }) - - test('shows correct icon and color for different document types', () => { - const documentTypes = [ - { type: 'prp', expectedClass: 'text-blue-600' }, - { type: 'technical', expectedClass: 'text-green-600' }, - { type: 'business', expectedClass: 'text-purple-600' }, - { type: 'meeting_notes', expectedClass: 'text-orange-600' }, - ] - - documentTypes.forEach(({ type, expectedClass }) => { - const { container, rerender } = render( - - ) - - const badge = container.querySelector(`.${expectedClass}`) - expect(badge).toBeInTheDocument() - }) - }) - - test('applies active styles when selected', () => { - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - expect(card.className).toContain('border-blue-500') - expect(card.className).toContain('scale-105') - }) - - test('calls onSelect when clicked', () => { - render( - - ) - - const card = screen.getByText('Test Document').closest('div') - fireEvent.click(card!) - - expect(mockHandlers.onSelect).toHaveBeenCalledWith(mockDocument) - }) - - test('shows delete button on hover', () => { - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - - // Delete button should not be visible initially - expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument() - - // Hover over the card - fireEvent.mouseEnter(card) - - // Delete button should now be visible - expect(screen.getByLabelText('Delete Test Document')).toBeInTheDocument() - - // Mouse leave - fireEvent.mouseLeave(card) - - // Delete button should be hidden again - expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument() - }) - - test('does not show delete button on active card', () => { - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - fireEvent.mouseEnter(card) - - expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument() - }) - - test('confirms before deleting', () => { - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) - - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - fireEvent.mouseEnter(card) - - const deleteButton = screen.getByLabelText('Delete Test Document') - fireEvent.click(deleteButton) - - expect(confirmSpy).toHaveBeenCalledWith('Delete "Test Document"?') - expect(mockHandlers.onDelete).toHaveBeenCalledWith('doc-1') - - confirmSpy.mockRestore() - }) - - test('cancels delete when user declines', () => { - const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) - - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - fireEvent.mouseEnter(card) - - const deleteButton = screen.getByLabelText('Delete Test Document') - fireEvent.click(deleteButton) - - expect(confirmSpy).toHaveBeenCalled() - expect(mockHandlers.onDelete).not.toHaveBeenCalled() - - confirmSpy.mockRestore() - }) - - test('applies dark mode styles correctly', () => { - const { container } = render( - - ) - - const card = container.firstChild as HTMLElement - expect(card.className).toContain('dark:') - }) -}) - -describe('NewDocumentCard', () => { - test('renders new document card', () => { - const onClick = vi.fn() - render() - - expect(screen.getByText('New Document')).toBeInTheDocument() - }) - - test('calls onClick when clicked', () => { - const onClick = vi.fn() - render() - - const card = screen.getByText('New Document').closest('div') - fireEvent.click(card!) - - expect(onClick).toHaveBeenCalledTimes(1) - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx b/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx deleted file mode 100644 index 0fe48778..00000000 --- a/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { describe, test, expect } from 'vitest' - -// Test the PRP to Markdown conversion logic -describe('MilkdownEditor PRP Conversion', () => { - // Helper function to format values (extracted from component) - const formatValue = (value: any, indent = ''): string => { - if (Array.isArray(value)) { - return value.map(item => `${indent}- ${formatValue(item, indent + ' ')}`).join('\n') + '\n' - } - - if (typeof value === 'object' && value !== null) { - let result = '' - Object.entries(value).forEach(([key, val]) => { - const formattedKey = key.replace(/_/g, ' ') - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - - if (typeof val === 'string' || typeof val === 'number') { - result += `${indent}**${formattedKey}:** ${val}\n\n` - } else { - result += `${indent}### ${formattedKey}\n\n${formatValue(val, indent)}` - } - }) - return result - } - - return String(value) - } - - // Simplified version of convertPRPToMarkdown for testing - const convertPRPToMarkdown = (content: any, docTitle = 'Test Doc'): string => { - let markdown = `# ${content.title || docTitle}\n\n` - - // Metadata section - if (content.version || content.author || content.date || content.status) { - markdown += `## Metadata\n\n` - if (content.version) markdown += `- **Version:** ${content.version}\n` - if (content.author) markdown += `- **Author:** ${content.author}\n` - if (content.date) markdown += `- **Date:** ${content.date}\n` - if (content.status) markdown += `- **Status:** ${content.status}\n` - markdown += '\n' - } - - // Goal section - if (content.goal) { - markdown += `## Goal\n\n${content.goal}\n\n` - } - - // Why section - if (content.why) { - markdown += `## Why\n\n` - if (Array.isArray(content.why)) { - content.why.forEach(item => markdown += `- ${item}\n`) - } else { - markdown += `${content.why}\n` - } - markdown += '\n' - } - - // What section - if (content.what) { - markdown += `## What\n\n` - if (typeof content.what === 'string') { - markdown += `${content.what}\n\n` - } else if (content.what.description) { - markdown += `${content.what.description}\n\n` - - if (content.what.success_criteria) { - markdown += `### Success Criteria\n\n` - content.what.success_criteria.forEach((criterion: string) => { - markdown += `- [ ] ${criterion}\n` - }) - markdown += '\n' - } - } - } - - // Handle all other sections dynamically - const handledKeys = [ - 'title', 'version', 'author', 'date', 'status', 'goal', 'why', 'what', - 'document_type' - ] - - Object.entries(content).forEach(([key, value]) => { - if (!handledKeys.includes(key) && value) { - const sectionTitle = key.replace(/_/g, ' ') - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - - markdown += `## ${sectionTitle}\n\n` - markdown += formatValue(value) - markdown += '\n' - } - }) - - return markdown - } - - test('converts basic PRP structure to markdown', () => { - const prp = { - title: 'Test PRP', - version: '1.0', - author: 'Test Author', - date: '2025-07-30', - status: 'draft', - goal: 'Test goal' - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('# Test PRP') - expect(markdown).toContain('## Metadata') - expect(markdown).toContain('- **Version:** 1.0') - expect(markdown).toContain('- **Author:** Test Author') - expect(markdown).toContain('- **Date:** 2025-07-30') - expect(markdown).toContain('- **Status:** draft') - expect(markdown).toContain('## Goal\n\nTest goal') - }) - - test('handles array why section', () => { - const prp = { - title: 'Test PRP', - why: ['Reason 1', 'Reason 2', 'Reason 3'] - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('## Why') - expect(markdown).toContain('- Reason 1') - expect(markdown).toContain('- Reason 2') - expect(markdown).toContain('- Reason 3') - }) - - test('handles string why section', () => { - const prp = { - title: 'Test PRP', - why: 'Single reason for the change' - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('## Why') - expect(markdown).toContain('Single reason for the change') - }) - - test('handles complex what section with success criteria', () => { - const prp = { - title: 'Test PRP', - what: { - description: 'Main description of what we are building', - success_criteria: [ - 'Criterion 1', - 'Criterion 2', - 'Criterion 3' - ] - } - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('## What') - expect(markdown).toContain('Main description of what we are building') - expect(markdown).toContain('### Success Criteria') - expect(markdown).toContain('- [ ] Criterion 1') - expect(markdown).toContain('- [ ] Criterion 2') - expect(markdown).toContain('- [ ] Criterion 3') - }) - - test('handles dynamic sections', () => { - const prp = { - title: 'Test PRP', - user_personas: { - developer: { - name: 'Developer Dan', - goals: ['Write clean code', 'Ship features fast'] - } - }, - technical_requirements: { - frontend: 'React 18', - backend: 'FastAPI', - database: 'PostgreSQL' - } - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('## User Personas') - expect(markdown).toContain('### Developer') - expect(markdown).toContain('**Name:** Developer Dan') - expect(markdown).toContain('## Technical Requirements') - expect(markdown).toContain('**Frontend:** React 18') - expect(markdown).toContain('**Backend:** FastAPI') - }) - - test('formats nested objects correctly', () => { - const value = { - level1: { - level2: { - level3: 'Deep value' - } - } - } - - const formatted = formatValue(value) - - expect(formatted).toContain('### Level1') - expect(formatted).toContain('### Level2') - expect(formatted).toContain('**Level3:** Deep value') - }) - - test('formats arrays correctly', () => { - const value = ['Item 1', 'Item 2', { nested: 'Nested item' }] - - const formatted = formatValue(value) - - expect(formatted).toContain('- Item 1') - expect(formatted).toContain('- Item 2') - expect(formatted).toContain('**Nested:** Nested item') - }) - - test('handles empty content', () => { - const prp = {} - - const markdown = convertPRPToMarkdown(prp, 'Default Title') - - expect(markdown).toBe('# Default Title\n\n') - }) - - test('skips null and undefined values', () => { - const prp = { - title: 'Test PRP', - null_field: null, - undefined_field: undefined, - empty_string: '', - valid_field: 'Valid content' - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).not.toContain('Null Field') - expect(markdown).not.toContain('Undefined Field') - expect(markdown).not.toContain('Empty String') - expect(markdown).toContain('## Valid Field') - expect(markdown).toContain('Valid content') - }) - - test('converts snake_case to Title Case', () => { - const prp = { - title: 'Test PRP', - user_journey_mapping: 'Content', - api_endpoint_design: 'More content' - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('## User Journey Mapping') - expect(markdown).toContain('## Api Endpoint Design') - }) - - test('preserves markdown formatting in content', () => { - const prp = { - title: 'Test PRP', - description: '**Bold text** and *italic text* with `code`' - } - - const markdown = convertPRPToMarkdown(prp) - - expect(markdown).toContain('**Bold text** and *italic text* with `code`') - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx b/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx deleted file mode 100644 index 8f90811f..00000000 --- a/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -describe('TasksTab Drag and Drop Integration', () => { - it('should properly manage movingTaskIds during drag operations', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check that moveTask adds task to movingTaskIds - expect(fileContent).toContain('setMovingTaskIds(prev => new Set([...prev, taskId]))'); - - // Check that moveTask removes task from movingTaskIds in finally block - expect(fileContent).toContain('finally {'); - expect(fileContent).toMatch(/finally\s*{\s*\/\/\s*Remove from loading set\s*setMovingTaskIds/); - - // Check that the cleanup happens even on error - const moveTaskMatch = fileContent.match(/const moveTask[\s\S]*?\n{2}\};/); - expect(moveTaskMatch).toBeTruthy(); - if (moveTaskMatch) { - const moveTaskFunction = moveTaskMatch[0]; - expect(moveTaskFunction).toContain('try {'); - expect(moveTaskFunction).toContain('catch (error)'); - expect(moveTaskFunction).toContain('finally {'); - } - }); - - it('should pass movingTaskIds to TaskBoardView', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check that movingTaskIds is passed to TaskBoardView - expect(fileContent).toContain('movingTaskIds={movingTaskIds}'); - }); - - it('should handle task completion through moveTask', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check that completeTask calls moveTask - expect(fileContent).toMatch(/completeTask.*moveTask\(taskId, 'done'\)/s); - }); - - it('should have optimistic updates in moveTask', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check for optimistic update comment and implementation - expect(fileContent).toContain('// Optimistically update UI for immediate feedback'); - expect(fileContent).toContain('setTasks(prev => prev.map(task =>'); - }); - - it('should revert on error as indicated by comment', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check for revert comment - expect(fileContent).toContain('// Revert optimistic update - polling will sync correct state'); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/test/components/prp/PRPViewer.test.tsx b/archon-ui-main/test/components/prp/PRPViewer.test.tsx deleted file mode 100644 index 1112fe1a..00000000 --- a/archon-ui-main/test/components/prp/PRPViewer.test.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi } from 'vitest' -import React from 'react' -import { PRPViewer } from '../../../src/components/prp/PRPViewer' -import type { PRPContent } from '../../../src/components/prp/types/prp.types' - -describe('PRPViewer', () => { - const mockContent: PRPContent = { - title: 'Test PRP', - version: '1.0', - author: 'Test Author', - date: '2025-07-30', - status: 'draft', - goal: 'Test goal with [Image #1] placeholder', - why: 'Test reason with [Image #2] reference', - what: { - description: 'Test description with [Image #3] and [Image #4]', - success_criteria: ['Criterion 1', 'Criterion 2 with [Image #5]'] - }, - context: { - background: 'Background with [Image #6]', - objectives: ['Objective 1', 'Objective 2'] - } - } - - test('renders without [Image #N] placeholders', () => { - render() - - // Check that [Image #N] placeholders are replaced - expect(screen.queryByText(/\[Image #\d+\]/)).not.toBeInTheDocument() - - // Check that content is present - expect(screen.getByText(/Test goal/)).toBeInTheDocument() - expect(screen.getByText(/Test reason/)).toBeInTheDocument() - expect(screen.getByText(/Test description/)).toBeInTheDocument() - }) - - test('processes nested content with image placeholders', () => { - const { container } = render() - - // Check that the content has been processed - const htmlContent = container.innerHTML - - // Should not contain raw [Image #N] text - expect(htmlContent).not.toMatch(/\[Image #\d+\]/) - - // Should contain processed markdown image syntax - expect(htmlContent).toContain('Image 1') - expect(htmlContent).toContain('Image 2') - }) - - test('renders metadata section correctly', () => { - render() - - expect(screen.getByText('Test PRP')).toBeInTheDocument() - expect(screen.getByText('1.0')).toBeInTheDocument() - expect(screen.getByText('Test Author')).toBeInTheDocument() - expect(screen.getByText('draft')).toBeInTheDocument() - }) - - test('handles empty content gracefully', () => { - render() - - // Should render without errors - expect(screen.getByText(/Metadata/)).toBeInTheDocument() - }) - - test('handles null content', () => { - render() - - expect(screen.getByText('No PRP content available')).toBeInTheDocument() - }) - - test('handles string content in objects', () => { - const stringContent = { - title: 'String Test', - description: 'This has [Image #1] in it' - } - - render() - - // Should process the image placeholder - expect(screen.queryByText(/\[Image #1\]/)).not.toBeInTheDocument() - expect(screen.getByText(/This has/)).toBeInTheDocument() - }) - - test('handles array content with image placeholders', () => { - const arrayContent = { - title: 'Array Test', - items: [ - 'Item 1 with [Image #1]', - 'Item 2 with [Image #2]', - { nested: 'Nested with [Image #3]' } - ] - } - - render() - - // Should process all image placeholders - expect(screen.queryByText(/\[Image #\d+\]/)).not.toBeInTheDocument() - }) - - test('renders collapsible sections', () => { - render() - - // Find collapsible sections - const contextSection = screen.getByText('Context').closest('div') - expect(contextSection).toBeInTheDocument() - - // Should have chevron icon for collapsible sections - const chevrons = screen.getAllByTestId('chevron-icon') - expect(chevrons.length).toBeGreaterThan(0) - }) - - test('toggles section visibility', () => { - render() - - // Find a collapsible section header - const contextHeader = screen.getByText('Context').closest('button') - - // The section should be visible initially (defaultOpen for first 5 sections) - expect(screen.getByText(/Background with/)).toBeInTheDocument() - - // Click to collapse - fireEvent.click(contextHeader!) - - // Content should be hidden - expect(screen.queryByText(/Background with/)).not.toBeInTheDocument() - - // Click to expand - fireEvent.click(contextHeader!) - - // Content should be visible again - expect(screen.getByText(/Background with/)).toBeInTheDocument() - }) - - test('applies dark mode styles', () => { - const { container } = render() - - const viewer = container.querySelector('.prp-viewer') - expect(viewer?.className).toContain('dark') - }) - - test('uses section overrides when provided', () => { - const CustomSection = ({ data, title }: any) => ( -
-

{title}

-

Custom rendering of: {JSON.stringify(data)}

-
- ) - - const overrides = { - context: CustomSection - } - - render() - - expect(screen.getByTestId('custom-section')).toBeInTheDocument() - expect(screen.getByText(/Custom rendering of/)).toBeInTheDocument() - }) - - test('sorts sections by group', () => { - const complexContent = { - title: 'Complex PRP', - // These should be sorted in a specific order - validation_gates: { test: 'validation' }, - user_personas: { test: 'personas' }, - context: { test: 'context' }, - user_flows: { test: 'flows' }, - success_metrics: { test: 'metrics' } - } - - const { container } = render() - - // Get all section titles in order - const sectionTitles = Array.from( - container.querySelectorAll('h3') - ).map(el => el.textContent) - - // Context should come before personas - const contextIndex = sectionTitles.findIndex(t => t?.includes('Context')) - const personasIndex = sectionTitles.findIndex(t => t?.includes('Personas')) - - expect(contextIndex).toBeLessThan(personasIndex) - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/config/api.test.ts b/archon-ui-main/test/config/api.test.ts deleted file mode 100644 index f5243961..00000000 --- a/archon-ui-main/test/config/api.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * Tests for API configuration port requirements - */ - -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; - -describe('API Configuration', () => { - let originalEnv: any; - - beforeEach(() => { - // Save original environment - originalEnv = { ...import.meta.env }; - - // Clear the module cache to ensure fresh imports - vi.resetModules(); - }); - - afterEach(() => { - // Restore original environment - Object.keys(import.meta.env).forEach(key => { - delete (import.meta.env as any)[key]; - }); - Object.assign(import.meta.env, originalEnv); - }); - - describe('getApiUrl', () => { - it('should use VITE_API_URL when provided', async () => { - // Set VITE_API_URL - (import.meta.env as any).VITE_API_URL = 'http://custom-api:9999'; - - const { getApiUrl } = await import('../../src/config/api'); - expect(getApiUrl()).toBe('http://custom-api:9999'); - }); - - it('should return empty string in production mode', async () => { - // Set production mode - (import.meta.env as any).PROD = true; - - // It should not use VITE_API_URL - (import.meta.env as any).VITE_API_URL = 'http://custom-api:9999'; - - const { getApiUrl } = await import('../../src/config/api'); - expect(getApiUrl()).toBe(''); - }); - - it('should use default port 8181 when no port environment variables are set in development', async () => { - // Development mode without any port variables - delete (import.meta.env as any).PROD; - delete (import.meta.env as any).VITE_API_URL; - delete (import.meta.env as any).VITE_ARCHON_SERVER_PORT; - delete (import.meta.env as any).VITE_PORT; - delete (import.meta.env as any).ARCHON_SERVER_PORT; - - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - hostname: 'localhost' - }, - writable: true - }); - - const { getApiUrl } = await import('../../src/config/api'); - - expect(getApiUrl()).toBe('http://localhost:8181'); - }); - - it('should use VITE_ARCHON_SERVER_PORT when set in development', async () => { - // Development mode with custom port via VITE_ prefix - delete (import.meta.env as any).PROD; - delete (import.meta.env as any).VITE_API_URL; - (import.meta.env as any).VITE_ARCHON_SERVER_PORT = '9191'; - - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - hostname: 'localhost' - }, - writable: true - }); - - const { getApiUrl } = await import('../../src/config/api'); - expect(getApiUrl()).toBe('http://localhost:9191'); - }); - - it('should use custom port with https protocol', async () => { - // Development mode with custom port and https via VITE_ prefix - delete (import.meta.env as any).PROD; - delete (import.meta.env as any).VITE_API_URL; - (import.meta.env as any).VITE_ARCHON_SERVER_PORT = '8443'; - - // Mock window.location with https - Object.defineProperty(window, 'location', { - value: { - protocol: 'https:', - hostname: 'example.com' - }, - writable: true - }); - - const { getApiUrl } = await import('../../src/config/api'); - expect(getApiUrl()).toBe('https://example.com:8443'); - }); - }); - - - describe('Port validation', () => { - it('should handle various port formats', async () => { - const testCases = [ - { port: '80', expected: 'http://localhost:80' }, - { port: '443', expected: 'http://localhost:443' }, - { port: '3000', expected: 'http://localhost:3000' }, - { port: '8080', expected: 'http://localhost:8080' }, - { port: '65535', expected: 'http://localhost:65535' }, - ]; - - for (const { port, expected } of testCases) { - vi.resetModules(); - delete (import.meta.env as any).PROD; - delete (import.meta.env as any).VITE_API_URL; - (import.meta.env as any).VITE_ARCHON_SERVER_PORT = port; - - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - hostname: 'localhost' - }, - writable: true - }); - - const { getApiUrl } = await import('../../src/config/api'); - expect(getApiUrl()).toBe(expected); - } - }); - }); -}); - -describe('MCP Client Service Configuration', () => { - let originalEnv: any; - - beforeEach(() => { - originalEnv = { ...import.meta.env }; - vi.resetModules(); - }); - - afterEach(() => { - Object.keys(import.meta.env).forEach(key => { - delete (import.meta.env as any)[key]; - }); - Object.assign(import.meta.env, originalEnv); - }); - - it('should throw error when ARCHON_MCP_PORT is not set', async () => { - delete (import.meta.env as any).ARCHON_MCP_PORT; - - const { mcpClientService } = await import('../../src/services/mcpClientService'); - - await expect(mcpClientService.createArchonClient()).rejects.toThrow('ARCHON_MCP_PORT environment variable is required'); - await expect(mcpClientService.createArchonClient()).rejects.toThrow('Default value: 8051'); - }); - - it('should use ARCHON_MCP_PORT when set', async () => { - (import.meta.env as any).ARCHON_MCP_PORT = '9051'; - (import.meta.env as any).ARCHON_SERVER_PORT = '8181'; - - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - hostname: 'localhost' - }, - writable: true - }); - - // Mock the API call - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - id: 'test-id', - name: 'Archon', - transport_type: 'http', - connection_status: 'connected' - }) - }); - - const { mcpClientService } = await import('../../src/services/mcpClientService'); - - try { - await mcpClientService.createArchonClient(); - - // Verify the fetch was called with the correct URL - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/api/mcp/clients'), - expect.objectContaining({ - method: 'POST', - body: expect.stringContaining('9051') - }) - ); - } catch (error) { - // If it fails due to actual API call, that's okay for this test - // We're mainly testing that it constructs the URL correctly - expect(error).toBeDefined(); - } - }); -}); diff --git a/archon-ui-main/test/errors.test.tsx b/archon-ui-main/test/errors.test.tsx deleted file mode 100644 index 3971f4af..00000000 --- a/archon-ui-main/test/errors.test.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' -import React from 'react' -import { credentialsService } from '../src/services/credentialsService' - -describe('Error Handling Tests', () => { - test('api error simulation', () => { - const MockApiComponent = () => { - const [error, setError] = React.useState('') - const [loading, setLoading] = React.useState(false) - - const fetchData = async () => { - setLoading(true) - try { - // Simulate API error - throw new Error('Network error') - } catch (err) { - setError('Failed to load data') - } finally { - setLoading(false) - } - } - - return ( -
- - {loading &&
Loading...
} - {error &&
{error}
} -
- ) - } - - render() - - fireEvent.click(screen.getByText('Load Data')) - expect(screen.getByRole('alert')).toHaveTextContent('Failed to load data') - }) - - test('timeout error simulation', () => { - const MockTimeoutComponent = () => { - const [status, setStatus] = React.useState('idle') - - const handleTimeout = () => { - setStatus('loading') - setTimeout(() => { - setStatus('timeout') - }, 100) - } - - return ( -
- - {status === 'loading' &&
Loading...
} - {status === 'timeout' &&
Request timed out
} -
- ) - } - - render() - - fireEvent.click(screen.getByText('Start Request')) - expect(screen.getByText('Loading...')).toBeInTheDocument() - - // Wait for timeout - setTimeout(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Request timed out') - }, 150) - }) - - test('form validation errors', () => { - const MockFormErrors = () => { - const [values, setValues] = React.useState({ name: '', email: '' }) - const [errors, setErrors] = React.useState([]) - - const validate = () => { - const newErrors: string[] = [] - if (!values.name) newErrors.push('Name is required') - if (!values.email) newErrors.push('Email is required') - if (values.email && !values.email.includes('@')) { - newErrors.push('Invalid email format') - } - setErrors(newErrors) - } - - return ( -
- setValues({ ...values, name: e.target.value })} - /> - setValues({ ...values, email: e.target.value })} - /> - - {errors.length > 0 && ( -
- {errors.map((error, index) => ( -
{error}
- ))} -
- )} -
- ) - } - - render() - - // Submit empty form - fireEvent.click(screen.getByText('Submit')) - - const alert = screen.getByRole('alert') - expect(alert).toHaveTextContent('Name is required') - expect(alert).toHaveTextContent('Email is required') - }) - - test('connection error recovery', () => { - const MockConnection = () => { - const [connected, setConnected] = React.useState(true) - const [error, setError] = React.useState('') - - const handleDisconnect = () => { - setConnected(false) - setError('Connection lost') - } - - const handleReconnect = () => { - setConnected(true) - setError('') - } - - return ( -
-
Status: {connected ? 'Connected' : 'Disconnected'}
- {error &&
{error}
} - - -
- ) - } - - render() - - expect(screen.getByText('Status: Connected')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Simulate Disconnect')) - expect(screen.getByText('Status: Disconnected')).toBeInTheDocument() - expect(screen.getByRole('alert')).toHaveTextContent('Connection lost') - - fireEvent.click(screen.getByText('Reconnect')) - expect(screen.getByText('Status: Connected')).toBeInTheDocument() - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) - - test('user friendly error messages', () => { - const MockErrorMessages = () => { - const [errorType, setErrorType] = React.useState('') - - const getErrorMessage = (type: string) => { - switch (type) { - case '401': - return 'Please log in to continue' - case '403': - return "You don't have permission to access this" - case '404': - return "We couldn't find what you're looking for" - case '500': - return 'Something went wrong on our end' - default: - return '' - } - } - - return ( -
- - - - - {errorType && ( -
{getErrorMessage(errorType)}
- )} -
- ) - } - - render() - - fireEvent.click(screen.getByText('401 Error')) - expect(screen.getByRole('alert')).toHaveTextContent('Please log in to continue') - - fireEvent.click(screen.getByText('404 Error')) - expect(screen.getByRole('alert')).toHaveTextContent("We couldn't find what you're looking for") - - fireEvent.click(screen.getByText('500 Error')) - expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong on our end') - }) -}) - -describe('CredentialsService Error Handling', () => { - const originalFetch = global.fetch - - beforeEach(() => { - global.fetch = vi.fn() as any - }) - - afterEach(() => { - global.fetch = originalFetch - }) - - test('should handle network errors with context', async () => { - const mockError = new Error('Network request failed') - ;(global.fetch as any).mockRejectedValueOnce(mockError) - - await expect(credentialsService.createCredential({ - key: 'TEST_KEY', - value: 'test', - is_encrypted: false, - category: 'test' - })).rejects.toThrow(/Network error while creating credential 'test_key'/) - }) - - test('should preserve context in error messages', async () => { - const mockError = new Error('database error') - ;(global.fetch as any).mockRejectedValueOnce(mockError) - - await expect(credentialsService.updateCredential({ - key: 'OPENAI_API_KEY', - value: 'sk-test', - is_encrypted: true, - category: 'api_keys' - })).rejects.toThrow(/Updating credential 'OPENAI_API_KEY' failed/) - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/hooks/usePolling.test.ts b/archon-ui-main/test/hooks/usePolling.test.ts deleted file mode 100644 index f374da70..00000000 --- a/archon-ui-main/test/hooks/usePolling.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { renderHook, act, waitFor } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { usePolling } from '../../src/hooks/usePolling'; - -describe('usePolling Hook - REAL Tests', () => { - beforeEach(() => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - // Mock fetch globally - global.fetch = vi.fn(); - // Reset document visibility state - Object.defineProperty(document, 'hidden', { - value: false, - writable: true, - configurable: true - }); - Object.defineProperty(document, 'visibilityState', { - value: 'visible', - writable: true, - configurable: true - }); - }); - - afterEach(() => { - vi.clearAllTimers(); - vi.useRealTimers(); - vi.clearAllMocks(); - vi.restoreAllMocks(); - }); - - it('should poll the endpoint at specified intervals', async () => { - const mockResponse = { data: 'test' }; - (global.fetch as any).mockResolvedValue({ - ok: true, - status: 200, - json: async () => mockResponse, - headers: new Headers({ 'etag': '"v1"' }) - }); - - const { result } = renderHook(() => - usePolling('/api/test', { interval: 1000 }) - ); - - // Initially loading - expect(result.current.isLoading).toBe(true); - expect(result.current.data).toBeUndefined(); - - // Wait for first fetch to complete - await waitFor(() => { - expect(result.current.data).toEqual(mockResponse); - expect(result.current.isLoading).toBe(false); - }, { timeout: 5000 }); - - expect(global.fetch).toHaveBeenCalledTimes(1); - - // Advance timer to trigger second poll - await act(async () => { - vi.advanceTimersByTime(1000); - }); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(2); - }, { timeout: 5000 }); - - // Check ETag header was sent on second request - const secondCall = (global.fetch as any).mock.calls[1]; - expect(secondCall[1].headers['If-None-Match']).toBe('"v1"'); - }, 15000); - - it('should handle 304 Not Modified responses correctly', async () => { - const initialData = { value: 'initial' }; - - // First call returns data - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => initialData, - headers: new Headers({ 'etag': '"v1"' }) - }); - - const { result } = renderHook(() => - usePolling('/api/test', { interval: 1000 }) - ); - - await waitFor(() => { - expect(result.current.data).toEqual(initialData); - expect(result.current.isLoading).toBe(false); - }, { timeout: 5000 }); - - // Second call returns 304 Not Modified - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - status: 304, - json: async () => null, - headers: new Headers({ 'etag': '"v1"' }) - }); - - await act(async () => { - vi.advanceTimersByTime(1000); - }); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(2); - }, { timeout: 5000 }); - - // Data should remain unchanged after 304 - expect(result.current.data).toEqual(initialData); - }, 15000); - - it('should pause polling when tab becomes inactive', async () => { - // This test verifies that polling stops when the tab is hidden - // The hook behavior is complex due to multiple useEffect hooks - // so we'll just verify the key behavior: no excessive polling when hidden - - (global.fetch as any).mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ data: 'test' }), - headers: new Headers() - }); - - const { result } = renderHook(() => usePolling('/api/test', { interval: 1000 })); - - // Wait for initial fetch - await waitFor(() => { - expect(result.current.data).toEqual({ data: 'test' }); - expect(result.current.isLoading).toBe(false); - }, { timeout: 5000 }); - - // Clear the mock to start fresh - vi.clearAllMocks(); - - // Simulate tab becoming hidden - await act(async () => { - Object.defineProperty(document, 'visibilityState', { - value: 'hidden', - writable: true, - configurable: true - }); - Object.defineProperty(document, 'hidden', { - value: true, - writable: true, - configurable: true - }); - document.dispatchEvent(new Event('visibilitychange')); - }); - - // Advance timers significantly while hidden - await act(async () => { - vi.advanceTimersByTime(5000); - }); - - // Should have minimal or no calls while hidden (allowing for edge cases) - const hiddenCallCount = (global.fetch as any).mock.calls.length; - expect(hiddenCallCount).toBeLessThanOrEqual(1); - - // Simulate tab becoming visible again - await act(async () => { - Object.defineProperty(document, 'visibilityState', { - value: 'visible', - writable: true, - configurable: true - }); - Object.defineProperty(document, 'hidden', { - value: false, - writable: true, - configurable: true - }); - document.dispatchEvent(new Event('visibilitychange')); - }); - - // Should trigger immediate refetch when becoming visible - await waitFor(() => { - expect((global.fetch as any).mock.calls.length).toBeGreaterThan(hiddenCallCount); - }, { timeout: 5000 }); - }, 15000); - - it('should handle errors and retry with backoff', async () => { - // First call fails - (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); - - const { result } = renderHook(() => - usePolling('/api/test', { interval: 1000 }) - ); - - await waitFor(() => { - expect(result.current.error).toBeInstanceOf(Error); - expect(result.current.error?.message).toBe('Network error'); - expect(result.current.isLoading).toBe(false); - }, { timeout: 5000 }); - - expect(global.fetch).toHaveBeenCalledTimes(1); - - // Second call succeeds - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ data: 'recovered' }), - headers: new Headers() - }); - - // Advance timer for retry - await act(async () => { - vi.advanceTimersByTime(1000); - }); - - await waitFor(() => { - expect(result.current.data).toEqual({ data: 'recovered' }); - expect(result.current.error).toBeNull(); - }, { timeout: 5000 }); - }, 15000); - - it('should cleanup on unmount', async () => { - (global.fetch as any).mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ data: 'test' }), - headers: new Headers() - }); - - const { unmount, result } = renderHook(() => - usePolling('/api/test', { interval: 1000 }) - ); - - // Wait for initial fetch to complete - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(1); - expect(result.current.isLoading).toBe(false); - }, { timeout: 5000 }); - - // Clear any pending timers before unmount - vi.clearAllTimers(); - - unmount(); - - // Reset mocks to clear call count - const callCountBeforeAdvance = (global.fetch as any).mock.calls.length; - - // Advance timers after unmount - await act(async () => { - vi.advanceTimersByTime(5000); - }); - - // No additional calls should be made after unmount - expect((global.fetch as any).mock.calls.length).toBe(callCountBeforeAdvance); - }, 15000); -}); \ No newline at end of file diff --git a/archon-ui-main/test/pages.test.tsx b/archon-ui-main/test/pages.test.tsx deleted file mode 100644 index bd7111be..00000000 --- a/archon-ui-main/test/pages.test.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, test, expect, vi } from 'vitest' -import React from 'react' -import { isLmConfigured } from '../src/utils/onboarding' -import type { NormalizedCredential } from '../src/utils/onboarding' - -// Mock useNavigate for onboarding page test -vi.mock('react-router-dom', () => ({ - useNavigate: () => vi.fn() -})) - -describe('Page Load Tests', () => { - test('simple page component renders', () => { - const MockPage = () =>

Projects

- render() - expect(screen.getByText('Projects')).toBeInTheDocument() - }) - - test('knowledge base mock renders', () => { - const MockKnowledgePage = () =>

Knowledge Base

- render() - expect(screen.getByText('Knowledge Base')).toBeInTheDocument() - }) - - test('settings mock renders', () => { - const MockSettingsPage = () =>

Settings

- render() - expect(screen.getByText('Settings')).toBeInTheDocument() - }) - - test('mcp mock renders', () => { - const MockMCPPage = () =>

MCP Servers

- render() - expect(screen.getByText('MCP Servers')).toBeInTheDocument() - }) - - test('tasks mock renders', () => { - const MockTasksPage = () => ( -
-

Tasks

-
TODO
-
In Progress
-
Done
-
- ) - render() - expect(screen.getByText('Tasks')).toBeInTheDocument() - expect(screen.getByText('TODO')).toBeInTheDocument() - expect(screen.getByText('In Progress')).toBeInTheDocument() - expect(screen.getByText('Done')).toBeInTheDocument() - }) - - test('onboarding page renders', () => { - const MockOnboardingPage = () =>

Welcome to Archon

- render() - expect(screen.getByText('Welcome to Archon')).toBeInTheDocument() - }) -}) - -describe('Onboarding Detection Tests', () => { - test('isLmConfigured returns true when provider is openai and OPENAI_API_KEY exists', () => { - const ragCreds: NormalizedCredential[] = [ - { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' } - ] - const apiKeyCreds: NormalizedCredential[] = [ - { key: 'OPENAI_API_KEY', value: 'sk-test123', category: 'api_keys' } - ] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true) - }) - - test('isLmConfigured returns true when provider is openai and OPENAI_API_KEY is encrypted', () => { - const ragCreds: NormalizedCredential[] = [ - { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' } - ] - const apiKeyCreds: NormalizedCredential[] = [ - { key: 'OPENAI_API_KEY', is_encrypted: true, encrypted_value: 'encrypted_sk-test123', category: 'api_keys' } - ] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true) - }) - - test('isLmConfigured returns false when provider is openai and no OPENAI_API_KEY', () => { - const ragCreds: NormalizedCredential[] = [ - { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' } - ] - const apiKeyCreds: NormalizedCredential[] = [] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(false) - }) - - test('isLmConfigured returns true when provider is ollama regardless of API keys', () => { - const ragCreds: NormalizedCredential[] = [ - { key: 'LLM_PROVIDER', value: 'ollama', category: 'rag_strategy' } - ] - const apiKeyCreds: NormalizedCredential[] = [] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true) - }) - - test('isLmConfigured returns true when no provider but OPENAI_API_KEY exists', () => { - const ragCreds: NormalizedCredential[] = [] - const apiKeyCreds: NormalizedCredential[] = [ - { key: 'OPENAI_API_KEY', value: 'sk-test123', category: 'api_keys' } - ] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true) - }) - - test('isLmConfigured returns false when no provider and no OPENAI_API_KEY', () => { - const ragCreds: NormalizedCredential[] = [] - const apiKeyCreds: NormalizedCredential[] = [] - - expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(false) - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/pages/ProjectPage.performance.test.tsx b/archon-ui-main/test/pages/ProjectPage.performance.test.tsx deleted file mode 100644 index 4019ee95..00000000 --- a/archon-ui-main/test/pages/ProjectPage.performance.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -describe('ProjectPage Performance Optimizations', () => { - const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx'); - const projectServicePath = join(process.cwd(), 'src/services/projectService.ts'); - - it('should use batch API call for task counts instead of N+1 queries', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Verify batch endpoint is being used - expect(fileContent).toContain('getTaskCountsForAllProjects'); - - // Verify we're NOT using Promise.allSettled for parallel fetching - expect(fileContent).not.toContain('Promise.allSettled'); - - // Verify single batch API call pattern - expect(fileContent).toContain('await projectService.getTaskCountsForAllProjects()'); - }); - - it('should have memoized handleProjectSelect to prevent duplicate calls', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check that handleProjectSelect is wrapped with useCallback - expect(fileContent).toMatch(/const handleProjectSelect = useCallback\(/); - - // Check for early return if same project - expect(fileContent).toContain('if (selectedProject?.id === project.id) return'); - - // Check dependency array includes selectedProject?.id - expect(fileContent).toMatch(/\}, \[.*selectedProject\?\.id.*\]\)/); - }); - - it('should implement task counts cache with TTL', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check cache ref is defined - expect(fileContent).toContain('const taskCountsCache = useRef'); - - // Check cache structure includes timestamp - expect(fileContent).toContain('timestamp: number'); - - // Check cache is checked before API call (5-minute TTL = 300000ms) - expect(fileContent).toContain('(now - taskCountsCache.current.timestamp) < 300000'); - - // Check cache is updated after successful API call - expect(fileContent).toContain('taskCountsCache.current = {'); - }); - - it('should disable polling during project switching and drag operations', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check useTaskPolling enabled parameter includes conditions - expect(fileContent).toMatch(/enabled:.*!isSwitchingProject.*movingTaskIds\.size === 0/); - - // Verify isSwitchingProject state exists - expect(fileContent).toContain('const [isSwitchingProject, setIsSwitchingProject]'); - }); - - it('should have debounce utility implemented', () => { - const debouncePath = join(process.cwd(), 'src/utils/debounce.ts'); - const fileContent = readFileSync(debouncePath, 'utf-8'); - - // Check debounce function exists - expect(fileContent).toContain('export function debounce'); - - // Check it has proper TypeScript types - expect(fileContent).toContain('T extends (...args: any[]) => any'); - - // Check timeout clearing logic - expect(fileContent).toContain('clearTimeout(timeoutId)'); - }); - - it('should apply debouncing to loadTaskCountsForAllProjects', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check debounce is imported - expect(fileContent).toContain('import { debounce } from "../utils/debounce"'); - - // Check debounced version is created - expect(fileContent).toContain('const debouncedLoadTaskCounts = useMemo'); - expect(fileContent).toContain('debounce((projectIds: string[])'); - - // Check debounced version is used instead of direct calls - expect(fileContent).toContain('debouncedLoadTaskCounts(projectIds)'); - - // Verify 1000ms delay - expect(fileContent).toContain('}, 1000)'); - }); - - it('should have batch task counts endpoint in backend service', () => { - const serviceContent = readFileSync(projectServicePath, 'utf-8'); - - // Check the service method exists - expect(serviceContent).toContain('async getTaskCountsForAllProjects()'); - - // Check it calls the correct endpoint - expect(serviceContent).toContain('/api/projects/task-counts'); - - // Check return type - expect(serviceContent).toContain('Promise>'); - }); - - it('should not make duplicate API calls on project switch', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check that tasks are cleared immediately on switch - expect(fileContent).toContain('setTasks([]); // Clear stale tasks immediately'); - - // Check loading state is managed properly - expect(fileContent).toContain('setIsSwitchingProject(true)'); - expect(fileContent).toContain('setIsSwitchingProject(false)'); - }); - - it('should have correct import statements for performance utilities', () => { - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check all necessary React hooks are imported - expect(fileContent).toContain('useCallback'); - expect(fileContent).toContain('useMemo'); - expect(fileContent).toContain('useRef'); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/test/pages/ProjectPage.polling.test.tsx b/archon-ui-main/test/pages/ProjectPage.polling.test.tsx deleted file mode 100644 index 43022150..00000000 --- a/archon-ui-main/test/pages/ProjectPage.polling.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -describe('ProjectPage Polling Conflict Prevention', () => { - it('should have movingTaskIds check in polling useEffect', () => { - // Read the actual source file to verify the implementation - const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx'); - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check that movingTaskIds state is declared - expect(fileContent).toContain('const [movingTaskIds, setMovingTaskIds] = useState>(new Set())'); - - // Check that movingTaskIds is checked before updating tasks - expect(fileContent).toContain('if (movingTaskIds.size === 0)'); - - // Check that merge logic is present for non-moving tasks - expect(fileContent).toContain('if (movingTaskIds.has(task.id))'); - expect(fileContent).toContain('return task; // Preserve local state for moving tasks'); - - // Check that movingTaskIds is in the dependency array - expect(fileContent).toMatch(/\}, \[.*movingTaskIds.*\]\)/); - }); - - it('should pass movingTaskIds props to TasksTab', () => { - const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx'); - const fileContent = readFileSync(projectPagePath, 'utf-8'); - - // Check that movingTaskIds is passed as prop - expect(fileContent).toContain('movingTaskIds={movingTaskIds}'); - expect(fileContent).toContain('setMovingTaskIds={setMovingTaskIds}'); - }); - - it('should have TasksTab accept movingTaskIds props', () => { - const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx'); - const fileContent = readFileSync(tasksTabPath, 'utf-8'); - - // Check that TasksTab accepts the props - expect(fileContent).toContain('movingTaskIds: Set'); - expect(fileContent).toContain('setMovingTaskIds: (ids: Set) => void'); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/test/services/projectService.test.ts b/archon-ui-main/test/services/projectService.test.ts deleted file mode 100644 index 98715954..00000000 --- a/archon-ui-main/test/services/projectService.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Unit tests for projectService document CRUD operations - */ - -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import type { Document } from '../../src/services/projectService'; - -// Mock fetch globally -global.fetch = vi.fn(); - -describe('projectService Document Operations', () => { - let projectService: any; - - beforeEach(async () => { - // Reset all mocks - vi.resetAllMocks(); - vi.resetModules(); - - // Import fresh instance of projectService - const module = await import('../../src/services/projectService'); - projectService = module.projectService; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('getDocument', () => { - const mockDocument: Document = { - id: 'doc-123', - project_id: 'proj-456', - title: 'Test Document', - content: { type: 'markdown', text: 'Test content' }, - document_type: 'prp', - metadata: { version: '1.0' }, - tags: ['test', 'sample'], - author: 'test-user', - created_at: '2025-08-18T10:00:00Z', - updated_at: '2025-08-18T10:00:00Z' - }; - - it('should successfully fetch a document', async () => { - // Mock successful response - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ document: mockDocument }) - }); - - const result = await projectService.getDocument('proj-456', 'doc-123'); - - expect(result).toEqual(mockDocument); - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs/doc-123', - expect.objectContaining({ - headers: expect.objectContaining({ - 'Content-Type': 'application/json' - }) - }) - ); - }); - - it('should include projectId in error message when fetch fails', async () => { - // Mock failed response - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - status: 404, - text: async () => '{"error": "Document not found"}' - }); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.getDocument('proj-456', 'doc-123')).rejects.toThrow(); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to get document doc-123 from project proj-456:', - expect.any(Error) - ); - - consoleSpy.mockRestore(); - }); - - it('should handle network errors', async () => { - // Mock network error - (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.getDocument('proj-456', 'doc-123')).rejects.toThrow('Network error'); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to get document doc-123 from project proj-456:', - expect.any(Error) - ); - - consoleSpy.mockRestore(); - }); - }); - - describe('updateDocument', () => { - const mockUpdatedDocument: Document = { - id: 'doc-123', - project_id: 'proj-456', - title: 'Updated Document', - content: { type: 'markdown', text: 'Updated content' }, - document_type: 'prp', - metadata: { version: '2.0' }, - tags: ['updated', 'test'], - author: 'test-user', - created_at: '2025-08-18T10:00:00Z', - updated_at: '2025-08-18T11:00:00Z' - }; - - const updates = { - title: 'Updated Document', - content: { type: 'markdown', text: 'Updated content' }, - tags: ['updated', 'test'] - }; - - it('should successfully update a document', async () => { - // Mock successful response - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ document: mockUpdatedDocument }) - }); - - const result = await projectService.updateDocument('proj-456', 'doc-123', updates); - - expect(result).toEqual(mockUpdatedDocument); - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs/doc-123', - expect.objectContaining({ - method: 'PUT', - headers: expect.objectContaining({ - 'Content-Type': 'application/json' - }), - body: JSON.stringify(updates) - }) - ); - }); - - it('should include projectId in error message when update fails', async () => { - // Mock failed response - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - status: 400, - text: async () => '{"error": "Invalid update data"}' - }); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.updateDocument('proj-456', 'doc-123', updates)).rejects.toThrow(); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to update document doc-123 in project proj-456:', - expect.any(Error) - ); - - consoleSpy.mockRestore(); - }); - - it('should handle partial updates', async () => { - const partialUpdate = { title: 'Only Title Updated' }; - - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ document: { ...mockUpdatedDocument, title: 'Only Title Updated' } }) - }); - - const result = await projectService.updateDocument('proj-456', 'doc-123', partialUpdate); - - expect(result.title).toBe('Only Title Updated'); - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs/doc-123', - expect.objectContaining({ - body: JSON.stringify(partialUpdate) - }) - ); - }); - }); - - describe('deleteDocument', () => { - it('should successfully delete a document', async () => { - // Mock successful response - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({}) - }); - - await expect(projectService.deleteDocument('proj-456', 'doc-123')).resolves.toBeUndefined(); - - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs/doc-123', - expect.objectContaining({ - method: 'DELETE', - headers: expect.objectContaining({ - 'Content-Type': 'application/json' - }) - }) - ); - }); - - it('should include projectId in error message when deletion fails', async () => { - // Mock failed response - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - status: 403, - text: async () => '{"error": "Permission denied"}' - }); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow(); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to delete document doc-123 from project proj-456:', - expect.any(Error) - ); - - consoleSpy.mockRestore(); - }); - - it('should handle 404 errors appropriately', async () => { - // Mock 404 response - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - status: 404, - text: async () => '{"error": "Document not found"}' - }); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow(); - - // Verify the error is logged with project context - expect(consoleSpy).toHaveBeenCalled(); - const errorLog = consoleSpy.mock.calls[0]; - expect(errorLog[0]).toContain('proj-456'); - expect(errorLog[0]).toContain('doc-123'); - - consoleSpy.mockRestore(); - }); - - it('should handle network timeouts', async () => { - // Mock timeout error - const timeoutError = new Error('Request timeout'); - (global.fetch as any).mockRejectedValueOnce(timeoutError); - - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow('Failed to call API'); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to delete document doc-123 from project proj-456:', - expect.objectContaining({ - message: expect.stringContaining('Request timeout') - }) - ); - - consoleSpy.mockRestore(); - }); - }); - - describe('listProjectDocuments', () => { - const mockDocuments: Document[] = [ - { - id: 'doc-1', - project_id: 'proj-456', - title: 'Document 1', - content: { type: 'markdown', text: 'Content 1' }, - document_type: 'prp', - created_at: '2025-08-18T10:00:00Z', - updated_at: '2025-08-18T10:00:00Z' - }, - { - id: 'doc-2', - project_id: 'proj-456', - title: 'Document 2', - content: { type: 'markdown', text: 'Content 2' }, - document_type: 'spec', - created_at: '2025-08-18T11:00:00Z', - updated_at: '2025-08-18T11:00:00Z' - } - ]; - - it('should successfully list all project documents', async () => { - // Mock successful response - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ documents: mockDocuments }) - }); - - const result = await projectService.listProjectDocuments('proj-456'); - - expect(result).toEqual(mockDocuments); - expect(result).toHaveLength(2); - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs', - expect.objectContaining({ - headers: expect.objectContaining({ - 'Content-Type': 'application/json' - }) - }) - ); - }); - - it('should return empty array when no documents exist', async () => { - // Mock response with no documents - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ documents: [] }) - }); - - const result = await projectService.listProjectDocuments('proj-456'); - - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('should handle null documents field gracefully', async () => { - // Mock response with null documents - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ documents: null }) - }); - - const result = await projectService.listProjectDocuments('proj-456'); - - expect(result).toEqual([]); - }); - }); - - describe('createDocument', () => { - const newDocumentData = { - title: 'New Document', - content: { type: 'markdown', text: 'New content' }, - document_type: 'prp', - tags: ['new', 'test'] - }; - - const mockCreatedDocument: Document = { - id: 'doc-new', - project_id: 'proj-456', - ...newDocumentData, - author: 'test-user', - created_at: '2025-08-18T12:00:00Z', - updated_at: '2025-08-18T12:00:00Z' - }; - - it('should successfully create a new document', async () => { - // Mock successful response - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ document: mockCreatedDocument }) - }); - - const result = await projectService.createDocument('proj-456', newDocumentData); - - expect(result).toEqual(mockCreatedDocument); - expect(result.id).toBeDefined(); - expect(global.fetch).toHaveBeenCalledWith( - '/api/projects/proj-456/docs', - expect.objectContaining({ - method: 'POST', - headers: expect.objectContaining({ - 'Content-Type': 'application/json' - }), - body: JSON.stringify(newDocumentData) - }) - ); - }); - - it('should handle validation errors', async () => { - // Mock validation error response - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - status: 422, - text: async () => '{"error": "Title is required"}' - }); - - const invalidData = { content: 'Missing title' }; - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - await expect(projectService.createDocument('proj-456', invalidData)).rejects.toThrow(); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to create document for project proj-456:', - expect.any(Error) - ); - - consoleSpy.mockRestore(); - }); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/test/user_flows.test.tsx b/archon-ui-main/test/user_flows.test.tsx deleted file mode 100644 index 71e97dfd..00000000 --- a/archon-ui-main/test/user_flows.test.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi } from 'vitest' -import React from 'react' - -describe('User Flow Tests', () => { - test('create project flow mock', () => { - const MockCreateProject = () => { - const [project, setProject] = React.useState('') - return ( -
-

Create Project

- setProject(e.target.value)} - /> - -
- ) - } - - render() - expect(screen.getByText('Create Project')).toBeInTheDocument() - expect(screen.getByPlaceholderText('Project title')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument() - }) - - test('search functionality mock', () => { - const MockSearch = () => { - const [query, setQuery] = React.useState('') - return ( -
-

Search

- setQuery(e.target.value)} - /> - {query &&
Results for: {query}
} -
- ) - } - - render() - const input = screen.getByPlaceholderText('Search knowledge base') - fireEvent.change(input, { target: { value: 'test query' } }) - expect(screen.getByText('Results for: test query')).toBeInTheDocument() - }) - - test('settings toggle mock', () => { - const MockSettings = () => { - const [theme, setTheme] = React.useState('light') - return ( -
-

Settings

- -
- ) - } - - render() - const button = screen.getByText('Theme: light') - fireEvent.click(button) - expect(screen.getByText('Theme: dark')).toBeInTheDocument() - }) - - test('file upload mock', () => { - const MockUpload = () => { - const [uploaded, setUploaded] = React.useState(false) - return ( -
-

Upload Documents

- setUploaded(true)} data-testid="file-input" /> - {uploaded &&
File uploaded successfully
} -
- ) - } - - render() - const input = screen.getByTestId('file-input') - fireEvent.change(input) - expect(screen.getByText('File uploaded successfully')).toBeInTheDocument() - }) - - test('connection status mock', () => { - const MockConnection = () => { - const [connected, setConnected] = React.useState(true) - return ( -
-

Connection Status

-
{connected ? 'Connected' : 'Disconnected'}
- -
- ) - } - - render() - expect(screen.getByText('Connected')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Toggle Connection')) - expect(screen.getByText('Disconnected')).toBeInTheDocument() - }) - - test('task management mock', () => { - const MockTasks = () => { - const [tasks, setTasks] = React.useState(['Task 1', 'Task 2']) - const addTask = () => setTasks([...tasks, `Task ${tasks.length + 1}`]) - - return ( -
-

Task Management

- -
    - {tasks.map((task, index) => ( -
  • {task}
  • - ))} -
-
- ) - } - - render() - expect(screen.getByText('Task 1')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Add Task')) - expect(screen.getByText('Task 3')).toBeInTheDocument() - }) - - test('navigation mock', () => { - const MockNav = () => { - const [currentPage, setCurrentPage] = React.useState('home') - return ( -
- -
-

Current page: {currentPage}

-
-
- ) - } - - render() - expect(screen.getByText('Current page: home')).toBeInTheDocument() - - fireEvent.click(screen.getByText('Projects')) - expect(screen.getByText('Current page: projects')).toBeInTheDocument() - }) - - test('form validation mock', () => { - const MockForm = () => { - const [email, setEmail] = React.useState('') - const [error, setError] = React.useState('') - - const handleSubmit = () => { - if (!email.includes('@')) { - setError('Invalid email') - } else { - setError('') - } - } - - return ( -
-

Form Validation

- setEmail(e.target.value)} - /> - - {error &&
{error}
} -
- ) - } - - render() - const input = screen.getByPlaceholderText('Email') - - fireEvent.change(input, { target: { value: 'invalid' } }) - fireEvent.click(screen.getByText('Submit')) - expect(screen.getByRole('alert')).toHaveTextContent('Invalid email') - }) - - test('theme switching mock', () => { - const MockTheme = () => { - const [isDark, setIsDark] = React.useState(false) - return ( -
-

Theme Test

- -
- ) - } - - render() - const button = screen.getByText('Switch to Dark') - fireEvent.click(button) - expect(screen.getByText('Switch to Light')).toBeInTheDocument() - }) - - test('data filtering mock', () => { - const MockFilter = () => { - const [filter, setFilter] = React.useState('') - const items = ['Apple', 'Banana', 'Cherry'] - const filtered = items.filter(item => - item.toLowerCase().includes(filter.toLowerCase()) - ) - - return ( -
-

Filter Test

- setFilter(e.target.value)} - /> -
    - {filtered.map((item, index) => ( -
  • {item}
  • - ))} -
-
- ) - } - - render() - const input = screen.getByPlaceholderText('Filter items') - - fireEvent.change(input, { target: { value: 'a' } }) - expect(screen.getByText('Apple')).toBeInTheDocument() - expect(screen.getByText('Banana')).toBeInTheDocument() - expect(screen.queryByText('Cherry')).not.toBeInTheDocument() - }) -}) \ No newline at end of file diff --git a/archon-ui-main/test/utils/taskOrdering.test.ts b/archon-ui-main/test/utils/taskOrdering.test.ts deleted file mode 100644 index cb6816fb..00000000 --- a/archon-ui-main/test/utils/taskOrdering.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { calculateTaskOrder, calculateReorderPosition, getDefaultTaskOrder } from '../../src/utils/taskOrdering'; -import { Task } from '../../src/types/project'; - -// Mock task factory -const createMockTask = (id: string, task_order: number): Task => ({ - id, - title: `Task ${id}`, - description: '', - status: 'todo', - assignee: { name: 'Test User', avatar: '' }, - feature: '', - featureColor: '#3b82f6', - task_order, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - project_id: 'test-project' -}); - -describe('taskOrdering utilities', () => { - describe('calculateTaskOrder', () => { - it('does not mutate existingTasks', () => { - const existingTasks = [createMockTask('1', 200), createMockTask('2', 100)]; - const snapshot = existingTasks.map(t => t.task_order); - calculateTaskOrder({ position: 'first', existingTasks }); - expect(existingTasks.map(t => t.task_order)).toEqual(snapshot); - }); - - it('should return seed value for first task when no existing tasks', () => { - const result = calculateTaskOrder({ - position: 'first', - existingTasks: [] - }); - expect(result).toBe(65536); - }); - - it('should calculate first position correctly', () => { - const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)]; - const result = calculateTaskOrder({ - position: 'first', - existingTasks - }); - expect(result).toBe(50); // Math.floor(100 / 2) - }); - - it('should calculate last position correctly', () => { - const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)]; - const result = calculateTaskOrder({ - position: 'last', - existingTasks - }); - expect(result).toBe(1224); // 200 + 1024 - }); - - it('should calculate between position correctly', () => { - const result = calculateTaskOrder({ - position: 'between', - existingTasks: [], - beforeTaskOrder: 100, - afterTaskOrder: 200 - }); - expect(result).toBe(150); // Math.floor((100 + 200) / 2) - }); - }); - - describe('getDefaultTaskOrder', () => { - it('should return seed value when no existing tasks', () => { - const result = getDefaultTaskOrder([]); - expect(result).toBe(65536); - }); - - it('should return first position when existing tasks present', () => { - const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)]; - const result = getDefaultTaskOrder(existingTasks); - expect(result).toBe(50); // Math.floor(100 / 2) - }); - }); - - describe('calculateReorderPosition', () => { - const statusTasks = [ - createMockTask('1', 100), - createMockTask('2', 200), - createMockTask('3', 300) - ]; - - it('should calculate position for moving to first', () => { - const result = calculateReorderPosition(statusTasks, 1, 0); - expect(result).toBeLessThan(statusTasks[0].task_order); - }); - - it('should calculate position for moving to last', () => { - const result = calculateReorderPosition(statusTasks, 0, 2); - expect(result).toBeGreaterThan(statusTasks[2].task_order); - }); - - it('should calculate position for moving down within middle (1 -> 2)', () => { - const result = calculateReorderPosition(statusTasks, 1, 2); - // After excluding moving index 1, insert between 300 and end => should be >300 (or handled by "last" path) - expect(result).toBeGreaterThan(statusTasks[2].task_order); - }); - - it('should calculate position for moving up within middle (2 -> 1)', () => { - const result = calculateReorderPosition(statusTasks, 2, 1); - // With fixed neighbor calculation, this should work correctly - expect(result).toBeGreaterThan(statusTasks[0].task_order); // > 100 - expect(result).toBeLessThan(statusTasks[1].task_order); // < 200 - }); - - it('should calculate position for moving between items', () => { - const result = calculateReorderPosition(statusTasks, 0, 1); - // Moving task 0 (order 100) to position 1 should place it before task 1 (order 200) - // Since we removed the moving task, it should be between start and 200 - expect(result).toBeLessThan(statusTasks[1].task_order); // < 200 - expect(result).toBeGreaterThan(0); // > 0 - }); - - it('should return integer values only', () => { - const result1 = calculateReorderPosition(statusTasks, 1, 0); - const result2 = calculateReorderPosition(statusTasks, 0, 2); - const result3 = calculateReorderPosition(statusTasks, 2, 1); - - expect(Number.isInteger(result1)).toBe(true); - expect(Number.isInteger(result2)).toBe(true); - expect(Number.isInteger(result3)).toBe(true); - }); - - it('should handle bounds checking correctly', () => { - // Test with tasks that have equal order values (edge case) - const equalTasks = [ - createMockTask('1', 100), - createMockTask('2', 100) - ]; - const result = calculateReorderPosition(equalTasks, 0, 1); - expect(Number.isInteger(result)).toBe(true); - expect(result).toBeGreaterThan(100); - }); - }); -}); \ No newline at end of file diff --git a/archon-ui-main/tests/README.md b/archon-ui-main/tests/README.md new file mode 100644 index 00000000..16fee148 --- /dev/null +++ b/archon-ui-main/tests/README.md @@ -0,0 +1,58 @@ +# Test Structure + +## Test Organization + +We follow a hybrid testing strategy: + +### Unit Tests (Colocated) +Unit tests live next to the code they test in the `src/features` directory: +``` +src/features/projects/ +├── components/ +│ ├── ProjectCard.tsx +│ └── ProjectCard.test.tsx +``` + +### Integration Tests +Tests that cross multiple features/systems: +``` +tests/integration/ +└── api.integration.test.ts +``` + +### E2E Tests +Full user flow tests: +``` +tests/e2e/ +└── user-flows.e2e.test.ts +``` + +## Running Tests + +```bash +# Run all tests +npm run test + +# Run tests in watch mode +npm run test:watch + +# Run with coverage +npm run test:coverage + +# Run specific test file +npx vitest run src/features/ui/hooks/useSmartPolling.test.ts +``` + +## Test Naming Conventions + +- **Unit tests**: `ComponentName.test.tsx` or `hookName.test.ts` +- **Integration tests**: `feature.integration.test.ts` +- **E2E tests**: `flow-name.e2e.test.ts` + +## Test Setup + +Global test setup is in `tests/setup.ts` which: +- Sets environment variables +- Mocks fetch and localStorage +- Mocks DOM APIs +- Mocks external libraries (lucide-react) \ No newline at end of file diff --git a/archon-ui-main/test/setup.ts b/archon-ui-main/tests/setup.ts similarity index 61% rename from archon-ui-main/test/setup.ts rename to archon-ui-main/tests/setup.ts index 54a4ccb5..0fddd2b4 100644 --- a/archon-ui-main/test/setup.ts +++ b/archon-ui-main/tests/setup.ts @@ -35,17 +35,28 @@ Object.defineProperty(window, 'localStorage', { Element.prototype.scrollIntoView = vi.fn() window.HTMLElement.prototype.scrollIntoView = vi.fn() -// Mock lucide-react icons - create a proxy that returns icon name for any icon -vi.mock('lucide-react', () => { - return new Proxy({}, { - get: (target, prop) => { - if (typeof prop === 'string') { - return () => prop - } - return undefined - } - }) -}) +// Mock lucide-react icons - simple implementation +vi.mock('lucide-react', () => ({ + Trash2: () => 'Trash2', + X: () => 'X', + AlertCircle: () => 'AlertCircle', + Loader2: () => 'Loader2', + BookOpen: () => 'BookOpen', + Settings: () => 'Settings', + WifiOff: () => 'WifiOff', + ChevronDown: () => 'ChevronDown', + ChevronRight: () => 'ChevronRight', + Plus: () => 'Plus', + Search: () => 'Search', + Activity: () => 'Activity', + CheckCircle2: () => 'CheckCircle2', + ListTodo: () => 'ListTodo', + MoreHorizontal: () => 'MoreHorizontal', + Pin: () => 'Pin', + PinOff: () => 'PinOff', + Clipboard: () => 'Clipboard', + // Add more icons as needed +})) // Mock ResizeObserver global.ResizeObserver = vi.fn().mockImplementation(() => ({ diff --git a/archon-ui-main/tsconfig.json b/archon-ui-main/tsconfig.json index a7d46b8c..6db9f331 100644 --- a/archon-ui-main/tsconfig.json +++ b/archon-ui-main/tsconfig.json @@ -23,6 +23,6 @@ /* Path mapping */ "paths": { "@/*": ["./src/*"] } }, - "include": ["src", "test"], + "include": ["src", "tests"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/archon-ui-main/tsconfig.prod.json b/archon-ui-main/tsconfig.prod.json new file mode 100644 index 00000000..9dfb2834 --- /dev/null +++ b/archon-ui-main/tsconfig.prod.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/__tests__/**", + "**/tests/**", + "src/features/testing/**", + "test/**", + "tests/**", + "coverage/**", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/archon-ui-main/vite.config.ts b/archon-ui-main/vite.config.ts index 9a986523..329b98b7 100644 --- a/archon-ui-main/vite.config.ts +++ b/archon-ui-main/vite.config.ts @@ -321,15 +321,18 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { test: { globals: true, environment: 'jsdom', - setupFiles: './test/setup.ts', + setupFiles: './tests/setup.ts', css: true, + include: [ + 'src/**/*.{test,spec}.{ts,tsx}', // Tests colocated in features + 'tests/**/*.{test,spec}.{ts,tsx}' // Tests in tests directory + ], exclude: [ '**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**', - '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', - '**/*.test.{ts,tsx}', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*' ], env: { VITE_HOST: host, diff --git a/archon-ui-main/vitest.config.ts b/archon-ui-main/vitest.config.ts index c27ee106..51e20e1c 100644 --- a/archon-ui-main/vitest.config.ts +++ b/archon-ui-main/vitest.config.ts @@ -8,10 +8,12 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - setupFiles: './test/setup.ts', + setupFiles: './tests/setup.ts', include: [ - 'test/**/*.test.{ts,tsx}', - 'test/**/*.spec.{ts,tsx}' + 'src/**/*.test.{ts,tsx}', // Colocated tests in features + 'src/**/*.spec.{ts,tsx}', + 'tests/**/*.test.{ts,tsx}', // Tests in tests directory + 'tests/**/*.spec.{ts,tsx}' ], exclude: ['node_modules', 'dist', '.git', '.cache', 'test.backup', '*.backup/**', 'test-backups'], reporters: ['dot', 'json'], @@ -35,7 +37,7 @@ export default defineConfig({ reportOnFailure: true, // Generate coverage reports even when tests fail exclude: [ 'node_modules/', - 'test/', + 'tests/', '**/*.d.ts', '**/*.config.*', '**/mockData.ts',