Skip to content

Testing Guide

Testing strategies and practices for IfAI.

Overview

IfAI uses multiple testing approaches:

  • Unit Tests: Test functions and components in isolation
  • Integration Tests: Test module interactions
  • E2E Tests: Test full user workflows
  • Performance Tests: Ensure app responsiveness

Frontend Testing

Unit Testing with Vitest

Setup

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true
  }
})

Component Tests

typescript
// Component.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Button } from './Button'

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })
  
  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    await screen.getByText('Click me').click()
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

Store Tests

typescript
// stores/useChatStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useChatStore } from './useChatStore'

describe('useChatStore', () => {
  beforeEach(() => {
    useChatStore.getState().reset()
  })
  
  it('sends message', async () => {
    const { sendMessage } = useChatStore.getState()
    
    await sendMessage('Hello')
    
    expect(useChatStore.getState().messages).toHaveLength(1)
  })
})

E2E Testing with Playwright

Setup

bash
npm install -D @playwright/test
typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e',
  use: {
    baseURL: 'http://localhost:1420'
  }
})

E2E Test Example

typescript
// tests/e2e/ai-chat.spec.ts
import { test, expect } from '@playwright/test'

test.describe('AI Chat', () => {
  test('sends and receives message', async ({ page }) => {
    await page.goto('/')
    
    // Open AI chat
    await page.keyboard.press('Cmd+K')
    
    // Type message
    await page.fill('[data-testid="chat-input"]', 'Hello')
    await page.click('[data-testid="send-button"]')
    
    // Wait for response
    await expect(page.locator('[data-testid="response"]')).toBeVisible()
  })
  
  test('displays code block with syntax highlighting', async ({ page }) => {
    await page.goto('/')
    await page.keyboard.press('Cmd+K')
    
    await page.fill('[data-testid="chat-input"]', 'Create a function')
    await page.click('[data-testid="send-button"]')
    
    // Check for code block
    await expect(page.locator('pre code')).toBeVisible()
    await expect(page.locator('pre code')).toHaveClass(/language-typescript/)
  })
})

Backend Testing

Rust Unit Tests

rust
// src/commands/atomic_commands_tests.rs
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_read_file() {
        let result = read_file("test.txt");
        assert!(result.is_ok());
    }
    
    #[test]
    fn test_write_file() {
        let result = write_file("test.txt", "content");
        assert!(result.is_ok());
    }
}

Integration Tests

rust
#[cfg(test)]
mod integration_tests {
    use super::*;
    
    #[tokio::test]
    async fn test_ai_chat_flow() {
        let messages = vec![
            Message { role: "user".into(), content: "Test".into() }
        ];
        
        let response = ai_chat(messages).await;
        assert!(response.is_ok());
    }
}

Test Organization

tests/
├── unit/                  # Unit tests
│   ├── components/        # Component tests
│   ├── stores/            # Store tests
│   └── services/          # Service tests
├── e2e/                   # E2E tests
│   ├── ai-chat.spec.ts    # AI chat tests
│   ├── composer.spec.ts   # Composer tests
│   └── navigation.spec.ts # Navigation tests
└── fixtures/              # Test data
    ├── sample-code/       # Sample projects
    └── mock-responses/    # Mock AI responses

Common Test Scenarios

AI Chat Tests

typescript
describe('AI Chat', () => {
  test('handles empty message')
  test('displays loading state')
  test('shows error on failure')
  test('supports slash commands')
  test('maintains conversation history')
})

Composer Tests

typescript
describe('Composer', () => {
  test('shows file list with changes')
  test('displays diff preview')
  test('accepts selected changes')
  test('rejects selected changes')
  test('applies all changes at once')
})

Code Navigation Tests

typescript
describe('Code Navigation', () => {
  test('goes to definition')
  test('finds references')
  test('searches symbols')
  test('handles undefined symbols')
  test('handles multiple definitions')
})

Performance Testing

Load Testing

typescript
// tests/performance/load-test.spec.ts
import { test } from '@playwright/test'

test('handles large file without lag', async ({ page }) => {
  // Open large file
  await page.goto('/?file=/test/large-file.ts')
  
  // Measure performance
  const metrics = await page.evaluate(() => {
    const start = performance.now()
    
    // Perform operation
    document.querySelector('.monaco-editor').click()
    
    return performance.now() - start
  })
  
  expect(metrics).toBeLessThan(100) // Should respond in <100ms
})

Memory Testing

bash
# Check memory usage
npm run test:memory

# Profile memory
npm run test:profile

Continuous Integration

GitHub Actions

yaml
# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]
        node-version: [18.x, 20.x]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      
      - name: Install dependencies
        run: npm install
      
      - name: Run linter
        run: npm run lint
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Run E2E tests
        run: npm run test:e2e

Best Practices

1. Test User Behavior, Not Implementation

typescript
// Good: Tests what user does
test('submits form when enter pressed', async () => {
  await page.fill('[name="email"]', 'test@example.com')
  await page.keyboard.press('Enter')
  await expect(page).toHaveURL('/success')
})

// Avoid: Tests implementation details
test('calls onSubmit function', () => {
  // Too tied to implementation
})

2. Use Page Objects

typescript
// tests/pages/ChatPage.ts
export class ChatPage {
  constructor(page: Page) {
    this.page = page
  }
  
  async open() {
    await this.page.goto('/')
    await this.page.keyboard.press('Cmd+K')
  }
  
  async sendMessage(text: string) {
    await this.page.fill('[data-testid="chat-input"]', text)
    await this.page.click('[data-testid="send-button"]')
  }
}

// Test using page object
test('sends message', async ({ page }) => {
  const chatPage = new ChatPage(page)
  await chatPage.open()
  await chatPage.sendMessage('Hello')
})

3. Mock External Dependencies

typescript
// Mock AI API
vi.mock('@/services/ai/aiClient', () => ({
  chat: vi.fn().mockResolvedValue('Mock response')
}))

// Test with mock
test('uses mocked AI', async () => {
  const { sendMessage } = useChatStore.getState()
  await sendMessage('Test')
  
  expect(chat).toHaveBeenCalledWith('Test')
})

Debugging Tests

UI Mode

bash
# Run tests in UI mode
npm run test:ui

Debug Mode

typescript
// Add debugger
test('debugging example', async ({ page }) => {
  debugger // Pause here in browser
  await page.goto('/')
})

Coverage

bash
# Generate coverage report
npm run test:coverage

# Open coverage report
open coverage/index.html

Next Steps

Released under the MIT License.