mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-30 21:49:30 -05:00
912 lines
25 KiB
Plaintext
912 lines
25 KiB
Plaintext
---
|
|
title: Vitest Testing Strategy
|
|
sidebar_position: 10
|
|
---
|
|
|
|
# Vitest Testing Strategy for React & TypeScript
|
|
|
|
This document outlines Archon's comprehensive frontend testing strategy using Vitest with React and TypeScript, incorporating the latest best practices and our current implementation.
|
|
|
|
## 🎯 Testing Philosophy
|
|
|
|
Our frontend testing follows these core principles:
|
|
|
|
1. **User-Centric Testing**: Test behavior from the user's perspective, not implementation details
|
|
2. **Component Isolation**: Each component test should be independent
|
|
3. **TypeScript Safety**: Leverage TypeScript for type-safe tests
|
|
4. **Fast Feedback**: Vitest's speed enables rapid test-driven development
|
|
5. **Meaningful Coverage**: Focus on critical user paths over coverage numbers
|
|
6. **Socket.IO Safety**: Never create real Socket.IO connections in tests
|
|
|
|
## 📁 Current Project Structure
|
|
|
|
```
|
|
archon-ui-main/
|
|
├── vitest.config.ts # Vitest configuration with coverage
|
|
├── vite.config.ts # Dev server with test endpoints
|
|
├── test/
|
|
│ ├── setup.ts # Enhanced test setup with comprehensive mocks
|
|
│ ├── README_TEST_GUIDE.md # Comprehensive testing guide
|
|
│ ├── TEST_STRUCTURE.md # File structure documentation
|
|
│ ├── components/ # Component tests (68 files planned)
|
|
│ │ ├── ui/ # UI component tests (8 files)
|
|
│ │ ├── layouts/ # Layout component tests (3 files)
|
|
│ │ ├── mcp/ # MCP component tests (3 files)
|
|
│ │ ├── settings/ # Settings component tests (5 files)
|
|
│ │ ├── project-tasks/ # Project task component tests (10 files)
|
|
│ │ ├── knowledge-base/ # Knowledge component tests (1 file)
|
|
│ │ └── animations/ # Animation component tests (1 file)
|
|
│ ├── services/ # Service layer tests (12 files)
|
|
│ ├── pages/ # Page component tests (5 files)
|
|
│ ├── hooks/ # Custom hook tests (2 files)
|
|
│ ├── contexts/ # Context provider tests (3 files)
|
|
│ ├── lib/ # Utility function tests (2 files)
|
|
│ ├── integration/ # Integration tests (8 files)
|
|
│ ├── e2e/ # End-to-end tests (5 files)
|
|
│ ├── performance/ # Performance tests (3 files)
|
|
│ ├── fixtures/ # Test data and mocks
|
|
│ └── utils/ # Test utilities
|
|
└── coverage/ # Generated coverage reports
|
|
├── index.html # HTML coverage report
|
|
├── coverage-summary.json # Coverage summary for UI
|
|
└── test-results.json # Test execution results
|
|
```
|
|
|
|
## 🔧 Configuration
|
|
|
|
### vitest.config.ts
|
|
|
|
```typescript
|
|
import { defineConfig } from 'vitest/config'
|
|
import react from '@vitejs/plugin-react'
|
|
import { resolve } from 'path'
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
resolve: {
|
|
alias: {
|
|
'@': resolve(__dirname, './src'),
|
|
'@test': resolve(__dirname, './test'),
|
|
},
|
|
},
|
|
test: {
|
|
globals: true,
|
|
environment: 'jsdom',
|
|
setupFiles: './test/setup.ts',
|
|
include: ['test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
exclude: ['node_modules', 'dist', '.git', '.cache'],
|
|
reporters: ['default', 'json'],
|
|
outputFile: {
|
|
json: './coverage/test-results.json'
|
|
},
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: [
|
|
'text-summary',
|
|
'html',
|
|
'json',
|
|
'json-summary',
|
|
'lcov'
|
|
],
|
|
reportsDirectory: './coverage',
|
|
thresholds: {
|
|
global: {
|
|
statements: 70,
|
|
branches: 65,
|
|
functions: 70,
|
|
lines: 70,
|
|
},
|
|
'src/services/**/*.ts': {
|
|
statements: 80,
|
|
branches: 75,
|
|
functions: 80,
|
|
lines: 80,
|
|
}
|
|
},
|
|
exclude: [
|
|
'src/**/*.d.ts',
|
|
'src/env.d.ts',
|
|
'src/types/**',
|
|
'test/**/*',
|
|
'node_modules/**',
|
|
],
|
|
},
|
|
// Improve test performance
|
|
pool: 'forks',
|
|
poolOptions: {
|
|
forks: {
|
|
singleFork: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### Enhanced Test Setup (test/setup.ts)
|
|
|
|
```typescript
|
|
import '@testing-library/jest-dom'
|
|
import { cleanup } from '@testing-library/react'
|
|
import { afterEach, beforeAll, afterAll, vi } from 'vitest'
|
|
import React from 'react'
|
|
|
|
// Clean up after each test
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// Comprehensive Lucide React icon mocks (170+ icons)
|
|
vi.mock('lucide-react', () => ({
|
|
// Core UI icons
|
|
Settings: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'settings-icon',
|
|
'data-lucide': 'Settings',
|
|
...props
|
|
}, 'Settings'),
|
|
|
|
User: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'user-icon',
|
|
'data-lucide': 'User',
|
|
...props
|
|
}, 'User'),
|
|
|
|
Bot: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'bot-icon',
|
|
'data-lucide': 'Bot',
|
|
...props
|
|
}, 'Bot'),
|
|
|
|
// Action icons
|
|
Play: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'play-icon',
|
|
'data-lucide': 'Play',
|
|
...props
|
|
}, 'Play'),
|
|
|
|
Pause: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'pause-icon',
|
|
'data-lucide': 'Pause',
|
|
...props
|
|
}, 'Pause'),
|
|
|
|
// Navigation icons
|
|
ChevronRight: ({ className, ...props }: any) =>
|
|
React.createElement('span', {
|
|
className,
|
|
'data-testid': 'chevron-right-icon',
|
|
'data-lucide': 'ChevronRight',
|
|
...props
|
|
}, 'ChevronRight'),
|
|
|
|
// ... and 164+ more icon mocks for complete coverage
|
|
}))
|
|
|
|
// Enhanced Socket.IO mocking with lifecycle simulation
|
|
export class MockSocketIO {
|
|
static CONNECTING = 0
|
|
static OPEN = 1
|
|
static CLOSING = 2
|
|
static CLOSED = 3
|
|
|
|
url: string
|
|
readyState: number = MockSocketIO.CONNECTING
|
|
onopen: ((event: Event) => void) | null = null
|
|
onclose: ((event: CloseEvent) => void) | null = null
|
|
onmessage: ((event: MessageEvent) => void) | null = null
|
|
onerror: ((event: Event) => void) | null = null
|
|
|
|
constructor(url: string) {
|
|
this.url = url
|
|
// Simulate connection opening asynchronously
|
|
setTimeout(() => {
|
|
this.readyState = MockSocketIO.OPEN
|
|
if (this.onopen) {
|
|
this.onopen(new Event('open'))
|
|
}
|
|
}, 0)
|
|
}
|
|
|
|
send = vi.fn()
|
|
close = vi.fn(() => {
|
|
this.readyState = MockSocketIO.CLOSED
|
|
if (this.onclose) {
|
|
this.onclose(new CloseEvent('close'))
|
|
}
|
|
})
|
|
|
|
addEventListener = vi.fn()
|
|
removeEventListener = vi.fn()
|
|
dispatchEvent = vi.fn()
|
|
}
|
|
|
|
// Replace global Socket.IO with mock
|
|
global.io = () => new MockSocketIO('') as any
|
|
|
|
// Enhanced Socket.IO service mock
|
|
vi.mock('@/services/websocketService', () => ({
|
|
websocketService: {
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
disconnect: vi.fn(),
|
|
subscribe: vi.fn().mockReturnValue(vi.fn()),
|
|
send: vi.fn(),
|
|
getConnectionState: vi.fn().mockReturnValue('connected'),
|
|
waitForConnection: vi.fn().mockResolvedValue(undefined),
|
|
isConnected: vi.fn().mockReturnValue(true),
|
|
reconnect: vi.fn().mockResolvedValue(undefined)
|
|
}
|
|
}))
|
|
|
|
// Mock window.matchMedia
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation(query => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
})
|
|
|
|
// Mock IntersectionObserver
|
|
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
|
observe: vi.fn(),
|
|
unobserve: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
}))
|
|
|
|
// Mock ResizeObserver
|
|
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|
observe: vi.fn(),
|
|
unobserve: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
}))
|
|
|
|
// Mock scrollIntoView
|
|
Element.prototype.scrollIntoView = vi.fn()
|
|
```
|
|
|
|
## 🧪 Testing Patterns
|
|
|
|
### 1. Component Testing
|
|
|
|
```typescript
|
|
// test/components/settings/TestStatus.test.tsx
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { TestStatus } from '@/components/settings/TestStatus'
|
|
import { testService } from '@/services/testService'
|
|
|
|
// Mock the test service
|
|
vi.mock('@/services/testService')
|
|
|
|
describe('TestStatus', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should show Test Results button after successful test run', async () => {
|
|
// Arrange
|
|
const mockCoverageData = {
|
|
total: { statements: { pct: 85 }, branches: { pct: 80 } }
|
|
}
|
|
const mockTestResults = {
|
|
numTotalTests: 10,
|
|
numPassedTests: 8,
|
|
numFailedTests: 2
|
|
}
|
|
|
|
vi.mocked(testService.hasTestResults).mockReturnValue(true)
|
|
vi.mocked(testService.getCoverageData).mockResolvedValue(mockCoverageData)
|
|
vi.mocked(testService.getTestResults).mockResolvedValue(mockTestResults)
|
|
|
|
// Act
|
|
render(<TestStatus />)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Results')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should hide Test Results button when no test results exist', () => {
|
|
// Arrange
|
|
vi.mocked(testService.hasTestResults).mockReturnValue(false)
|
|
|
|
// Act
|
|
render(<TestStatus />)
|
|
|
|
// Assert
|
|
expect(screen.queryByText('Test Results')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should open Test Results Modal when button is clicked', async () => {
|
|
// Arrange
|
|
const user = userEvent.setup()
|
|
vi.mocked(testService.hasTestResults).mockReturnValue(true)
|
|
|
|
render(<TestStatus />)
|
|
|
|
// Act
|
|
const button = screen.getByText('Test Results')
|
|
await user.click(button)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Health Score')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
### 2. Service Testing with Socket.IO Safety
|
|
|
|
```typescript
|
|
// test/services/websocketService.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { websocketService } from '@/services/websocketService'
|
|
|
|
// CRITICAL: Always mock Socket.IO service
|
|
vi.mock('@/services/websocketService', () => ({
|
|
websocketService: {
|
|
connect: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
subscribe: vi.fn(),
|
|
send: vi.fn(),
|
|
getConnectionState: vi.fn()
|
|
}
|
|
}))
|
|
|
|
describe('Socket.IO Service (Mocked)', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Critical: Clean up any subscriptions
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('should handle connection lifecycle', async () => {
|
|
// Arrange
|
|
const mockCallback = vi.fn()
|
|
vi.mocked(websocketService.connect).mockResolvedValue(undefined)
|
|
vi.mocked(websocketService.subscribe).mockReturnValue(vi.fn())
|
|
|
|
// Act
|
|
await websocketService.connect('/test-endpoint')
|
|
const unsubscribe = websocketService.subscribe('test-channel', mockCallback)
|
|
|
|
// Assert
|
|
expect(websocketService.connect).toHaveBeenCalledWith('/test-endpoint')
|
|
expect(websocketService.subscribe).toHaveBeenCalledWith('test-channel', mockCallback)
|
|
expect(typeof unsubscribe).toBe('function')
|
|
})
|
|
|
|
it('should handle message subscription', () => {
|
|
// Arrange
|
|
const mockHandler = vi.fn()
|
|
const mockUnsubscribe = vi.fn()
|
|
vi.mocked(websocketService.subscribe).mockReturnValue(mockUnsubscribe)
|
|
|
|
// Act
|
|
const unsubscribe = websocketService.subscribe('progress', mockHandler)
|
|
|
|
// Assert
|
|
expect(websocketService.subscribe).toHaveBeenCalledWith('progress', mockHandler)
|
|
expect(unsubscribe).toBe(mockUnsubscribe)
|
|
})
|
|
})
|
|
```
|
|
|
|
### 3. Hook Testing
|
|
|
|
```typescript
|
|
// test/hooks/useNeonGlow.test.ts
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { useNeonGlow } from '@/hooks/useNeonGlow'
|
|
|
|
describe('useNeonGlow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should initialize with default glow state', () => {
|
|
// Act
|
|
const { result } = renderHook(() => useNeonGlow())
|
|
|
|
// Assert
|
|
expect(result.current.isGlowing).toBe(false)
|
|
expect(typeof result.current.startGlow).toBe('function')
|
|
expect(typeof result.current.stopGlow).toBe('function')
|
|
})
|
|
|
|
it('should handle glow activation', () => {
|
|
// Arrange
|
|
const { result } = renderHook(() => useNeonGlow())
|
|
|
|
// Act
|
|
act(() => {
|
|
result.current.startGlow()
|
|
})
|
|
|
|
// Assert
|
|
expect(result.current.isGlowing).toBe(true)
|
|
})
|
|
|
|
it('should handle glow deactivation', () => {
|
|
// Arrange
|
|
const { result } = renderHook(() => useNeonGlow())
|
|
|
|
// Act
|
|
act(() => {
|
|
result.current.startGlow()
|
|
})
|
|
act(() => {
|
|
result.current.stopGlow()
|
|
})
|
|
|
|
// Assert
|
|
expect(result.current.isGlowing).toBe(false)
|
|
})
|
|
})
|
|
```
|
|
|
|
### 4. Integration Testing
|
|
|
|
```typescript
|
|
// test/integration/test-results-flow.test.tsx
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { SettingsPage } from '@/pages/SettingsPage'
|
|
import { testService } from '@/services/testService'
|
|
|
|
vi.mock('@/services/testService')
|
|
|
|
describe('Test Results Integration Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should show complete test results workflow', async () => {
|
|
// Arrange
|
|
const user = userEvent.setup()
|
|
const mockCoverageData = {
|
|
total: {
|
|
statements: { pct: 85 },
|
|
branches: { pct: 80 },
|
|
functions: { pct: 90 },
|
|
lines: { pct: 85 }
|
|
}
|
|
}
|
|
const mockTestResults = {
|
|
numTotalTests: 20,
|
|
numPassedTests: 18,
|
|
numFailedTests: 2,
|
|
testResults: [
|
|
{
|
|
name: 'Component Tests',
|
|
status: 'passed',
|
|
numPassedTests: 15,
|
|
numFailedTests: 0
|
|
},
|
|
{
|
|
name: 'Service Tests',
|
|
status: 'failed',
|
|
numPassedTests: 3,
|
|
numFailedTests: 2
|
|
}
|
|
]
|
|
}
|
|
|
|
vi.mocked(testService.hasTestResults).mockReturnValue(true)
|
|
vi.mocked(testService.getCoverageData).mockResolvedValue(mockCoverageData)
|
|
vi.mocked(testService.getTestResults).mockResolvedValue(mockTestResults)
|
|
|
|
// Act
|
|
render(<SettingsPage />)
|
|
|
|
// Navigate to test results
|
|
const testResultsButton = await screen.findByText('Test Results')
|
|
await user.click(testResultsButton)
|
|
|
|
// Assert modal appears with comprehensive data
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Health Score')).toBeInTheDocument()
|
|
expect(screen.getByText('90%')).toBeInTheDocument() // Health score
|
|
expect(screen.getByText('18 Passed')).toBeInTheDocument()
|
|
expect(screen.getByText('2 Failed')).toBeInTheDocument()
|
|
expect(screen.getByText('Statements: 85%')).toBeInTheDocument()
|
|
expect(screen.getByText('Component Tests')).toBeInTheDocument()
|
|
expect(screen.getByText('Service Tests')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
## 🎯 UI Test Runner Integration
|
|
|
|
Archon includes a sophisticated UI test runner accessible from the Settings page:
|
|
|
|
### Test Results Modal Features
|
|
|
|
```typescript
|
|
// Example of testing the Test Results Modal
|
|
describe('TestResultsModal', () => {
|
|
it('should display comprehensive test health information', async () => {
|
|
const mockData = {
|
|
testHealthScore: 90,
|
|
testSummary: {
|
|
totalTests: 20,
|
|
passedTests: 18,
|
|
failedTests: 2,
|
|
duration: 15.5
|
|
},
|
|
coverage: {
|
|
statements: 85,
|
|
branches: 80,
|
|
functions: 90,
|
|
lines: 85
|
|
},
|
|
testSuites: [
|
|
{ name: 'Components', status: 'passed', passed: 15, failed: 0 },
|
|
{ name: 'Services', status: 'failed', passed: 3, failed: 2 }
|
|
]
|
|
}
|
|
|
|
render(<TestResultsModal isOpen={true} data={mockData} onClose={vi.fn()} />)
|
|
|
|
expect(screen.getByText('Test Health Score: 90%')).toBeInTheDocument()
|
|
expect(screen.getByText('18 Passed, 2 Failed')).toBeInTheDocument()
|
|
expect(screen.getByText('Duration: 15.5s')).toBeInTheDocument()
|
|
|
|
// Coverage progress bars
|
|
expect(screen.getByText('Statements: 85%')).toBeInTheDocument()
|
|
expect(screen.getByText('Branches: 80%')).toBeInTheDocument()
|
|
|
|
// Individual test suites
|
|
expect(screen.getByText('Components')).toBeInTheDocument()
|
|
expect(screen.getByText('Services')).toBeInTheDocument()
|
|
})
|
|
})
|
|
```
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Navigate to frontend directory
|
|
cd archon-ui-main
|
|
|
|
# Run all tests
|
|
npm test
|
|
|
|
# Run tests with coverage and results generation
|
|
npm run test:coverage
|
|
|
|
# Run tests in watch mode
|
|
npm test -- --watch
|
|
|
|
# Run tests with UI interface
|
|
npm run test:ui
|
|
|
|
# Run specific test file
|
|
npm test -- TestStatus.test.tsx
|
|
|
|
# Run tests matching pattern
|
|
npm test -- --grep "should handle"
|
|
|
|
# Run tests for specific directory
|
|
npm test -- test/components/settings/
|
|
```
|
|
|
|
### Test Development Workflow
|
|
|
|
```bash
|
|
# 1. Create test file alongside component
|
|
touch test/components/ui/NewComponent.test.tsx
|
|
|
|
# 2. Run test in watch mode during development
|
|
npm test -- --watch NewComponent.test.tsx
|
|
|
|
# 3. Check coverage for the specific component
|
|
npm run test:coverage -- --reporter=text NewComponent.test.tsx
|
|
|
|
# 4. Run all tests before committing
|
|
npm run test:coverage
|
|
```
|
|
|
|
## 📊 Coverage Analysis
|
|
|
|
### Current Coverage Goals
|
|
|
|
```typescript
|
|
// vitest.config.ts coverage thresholds
|
|
coverage: {
|
|
thresholds: {
|
|
global: {
|
|
statements: 70,
|
|
branches: 65,
|
|
functions: 70,
|
|
lines: 70,
|
|
},
|
|
'src/services/**/*.ts': {
|
|
statements: 80,
|
|
branches: 75,
|
|
functions: 80,
|
|
lines: 80,
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Coverage Reports Generated
|
|
|
|
1. **HTML Report**: `coverage/index.html` - Interactive coverage browser
|
|
2. **JSON Summary**: `coverage/coverage-summary.json` - For UI integration
|
|
3. **LCOV**: `coverage/lcov.info` - For CI/CD integration
|
|
4. **Text Summary**: Console output during test runs
|
|
|
|
### Using Coverage Data in UI
|
|
|
|
```typescript
|
|
// Example of how Test Results Modal consumes coverage data
|
|
const TestResultsModal = () => {
|
|
const [coverageData, setCoverageData] = useState(null)
|
|
|
|
useEffect(() => {
|
|
const loadCoverageData = async () => {
|
|
try {
|
|
const data = await testService.getCoverageData()
|
|
setCoverageData(data)
|
|
} catch (error) {
|
|
console.error('Failed to load coverage data:', error)
|
|
}
|
|
}
|
|
|
|
loadCoverageData()
|
|
}, [])
|
|
|
|
const calculateHealthScore = (testResults, coverage) => {
|
|
const testSuccessRate = (testResults.numPassedTests / testResults.numTotalTests) * 100
|
|
const avgCoverage = (coverage.statements + coverage.branches + coverage.functions + coverage.lines) / 4
|
|
return Math.round((testSuccessRate + avgCoverage) / 2)
|
|
}
|
|
|
|
// ... render logic
|
|
}
|
|
```
|
|
|
|
## 🛠️ Socket.IO Testing Best Practices
|
|
|
|
### ⚠️ Critical Safety Rules
|
|
|
|
1. **NEVER create real Socket.IO connections in tests**
|
|
2. **ALWAYS mock the websocketService module**
|
|
3. **NEVER include Socket.IO functions in useCallback dependencies**
|
|
4. **ALWAYS clean up subscriptions in afterEach**
|
|
|
|
### Safe Socket.IO Testing Pattern
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Always mock the service
|
|
vi.mock('@/services/websocketService', () => ({
|
|
websocketService: {
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
disconnect: vi.fn(),
|
|
subscribe: vi.fn().mockReturnValue(vi.fn()),
|
|
send: vi.fn(),
|
|
getConnectionState: vi.fn().mockReturnValue('connected')
|
|
}
|
|
}))
|
|
|
|
// ✅ CORRECT: Test Socket.IO interactions safely
|
|
it('should handle Socket.IO message updates', async () => {
|
|
const mockCallback = vi.fn()
|
|
let capturedCallback: Function
|
|
|
|
vi.mocked(websocketService.subscribe).mockImplementation((channel, callback) => {
|
|
capturedCallback = callback
|
|
return vi.fn() // unsubscribe function
|
|
})
|
|
|
|
render(<ComponentWithSocketIO />)
|
|
|
|
// Simulate Socket.IO message
|
|
act(() => {
|
|
capturedCallback!({ type: 'progress', data: { percent: 50 } })
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Progress: 50%')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ❌ WRONG: Never create real Socket.IO connections
|
|
it('should connect to real Socket.IO', () => {
|
|
const socket = io('http://localhost:8080') // DON'T DO THIS
|
|
// This will break tests and potentially affect running services
|
|
})
|
|
```
|
|
|
|
## 🎯 Test Implementation Status
|
|
|
|
### Completed Tests ✅
|
|
|
|
- `test/App.test.tsx` - Basic app rendering
|
|
- `test/services/api.test.ts` - API service functionality
|
|
- `test/services/mcpService.test.ts` - MCP service operations
|
|
- `test/services/knowledgeBaseService.test.ts` - Knowledge base operations
|
|
- `test/pages/MCPPage.test.tsx` - MCP page rendering
|
|
- `test/pages/KnowledgeBasePage.test.tsx` - Knowledge base page
|
|
- `test/pages/CrawlingProgress.test.tsx` - Crawling progress page
|
|
|
|
### High Priority (Next Phase) 📝
|
|
|
|
1. **Services Layer** (Critical)
|
|
- `socketioService.test.ts` - Socket.IO connection management
|
|
- `projectService.test.ts` - Project CRUD operations
|
|
- `testService.test.ts` - Test execution and results
|
|
- `credentialsService.test.ts` - Credentials management
|
|
|
|
2. **Settings Components** (High)
|
|
- `TestStatus.test.tsx` - Test Results Modal integration
|
|
- `APIKeysSection.test.tsx` - API key management
|
|
- `FeaturesSection.test.tsx` - Feature toggles
|
|
- `RAGSettings.test.tsx` - RAG configuration
|
|
|
|
3. **Project Components** (High)
|
|
- `TasksTab.test.tsx` - Task management interface
|
|
- `TaskTableView.test.tsx` - Task table functionality
|
|
- `TaskBoardView.test.tsx` - Kanban board interface
|
|
|
|
### Coverage Progress
|
|
|
|
- **Total Files Planned**: 68 test files
|
|
- **Currently Implemented**: 7 files (10%)
|
|
- **Target Coverage**: 80% overall, 90% for critical paths
|
|
- **Current Coverage**: ~15% overall
|
|
|
|
## 🔄 CI/CD Integration
|
|
|
|
### GitHub Actions Workflow
|
|
|
|
```yaml
|
|
name: Frontend Tests
|
|
|
|
on:
|
|
push:
|
|
branches: [main, develop]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
cache-dependency-path: archon-ui-main/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
working-directory: ./archon-ui-main
|
|
run: npm ci
|
|
|
|
- name: Type check
|
|
working-directory: ./archon-ui-main
|
|
run: npm run type-check
|
|
|
|
- name: Run tests with coverage
|
|
working-directory: ./archon-ui-main
|
|
run: npm run test:coverage
|
|
|
|
- name: Upload coverage reports
|
|
uses: codecov/codecov-action@v3
|
|
with:
|
|
file: ./archon-ui-main/coverage/coverage-final.json
|
|
flags: frontend
|
|
fail_ci_if_error: true
|
|
```
|
|
|
|
## 📚 Best Practices Checklist
|
|
|
|
### Before Writing Tests
|
|
- [ ] Component/service is implemented and working
|
|
- [ ] Identify critical user paths to test
|
|
- [ ] Plan test scenarios (happy path, error cases, edge cases)
|
|
- [ ] Ensure Socket.IO mocking is in place if needed
|
|
|
|
### Writing Tests
|
|
- [ ] Use descriptive test names (`should do X when Y happens`)
|
|
- [ ] Follow AAA pattern (Arrange, Act, Assert)
|
|
- [ ] Test user behavior, not implementation details
|
|
- [ ] Mock external dependencies appropriately
|
|
- [ ] Use proper TypeScript types in tests
|
|
|
|
### After Writing Tests
|
|
- [ ] Tests pass consistently
|
|
- [ ] Coverage meets threshold requirements
|
|
- [ ] No console errors or warnings
|
|
- [ ] Tests run quickly (under 100ms per test ideally)
|
|
- [ ] Clean up resources in afterEach hooks
|
|
|
|
### Socket.IO-Specific Checklist
|
|
- [ ] Socket.IO service is mocked, never real connections
|
|
- [ ] Subscription cleanup is handled
|
|
- [ ] No function references in useCallback dependencies
|
|
- [ ] Connection state changes are tested
|
|
- [ ] Error scenarios are covered
|
|
|
|
## 🛠️ Troubleshooting Common Issues
|
|
|
|
### Test Environment Issues
|
|
|
|
```typescript
|
|
// Issue: Icons not rendering in tests
|
|
// Solution: Comprehensive Lucide React mocking in setup.ts
|
|
|
|
// Issue: Socket.IO connection errors in tests
|
|
// Solution: Always mock websocketService module
|
|
|
|
// Issue: Async timing issues
|
|
// Solution: Use waitFor and findBy queries
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Expected text')).toBeInTheDocument()
|
|
})
|
|
|
|
// Issue: State update warnings
|
|
// Solution: Wrap state updates in act()
|
|
act(() => {
|
|
// state updates
|
|
})
|
|
```
|
|
|
|
### Coverage Issues
|
|
|
|
```bash
|
|
# Issue: Low coverage on specific files
|
|
# Solution: Check what's not covered
|
|
npm run test:coverage -- --reporter=text-summary
|
|
|
|
# Issue: Coverage threshold failures
|
|
# Solution: Either improve tests or adjust thresholds in vitest.config.ts
|
|
```
|
|
|
|
## 🔗 Related Documentation
|
|
|
|
- **[Testing Overview](./testing)** - General testing strategy and architecture
|
|
- **[Python Testing Strategy](./testing-python-strategy)** - Backend testing guide
|
|
- **[Socket.IO Documentation](./socketio)** - Real-time communication patterns
|
|
- **[UI Documentation](./ui)** - Component design and usage guidelines
|
|
|
|
---
|
|
|
|
**Quick Navigation:**
|
|
- 🚀 [Quick Start](#quick-start) - Get started with testing immediately
|
|
- 🎯 [UI Test Runner Integration](#ui-test-runner-integration) - Use the Settings page test runner
|
|
- 🛠️ [Socket.IO Testing](#socketio-testing-best-practices) - Safe Socket.IO testing patterns
|
|
- 📊 [Coverage Analysis](#coverage-analysis) - Understanding and improving coverage |