- Update PLAN.md with current architecture - Update README with new setup instructions - Update SYSTEM_TRAY and TESTING docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7.8 KiB
7.8 KiB
Testing Guide
This document covers the complete testing infrastructure for the Lilith AI Desktop Chat App.
Quick Start
# Run all tests in watch mode
pnpm test
# Run tests once (CI mode)
pnpm test:run
# Run with coverage report
pnpm test:coverage
# Open Vitest UI
pnpm test:ui
# Run E2E tests (Playwright + Electron)
pnpm test:e2e
# Run E2E with UI
pnpm test:e2e:ui
Run Specific Tests
# Single test file
pnpm test -- src/renderer/stores/__tests__/conversationStore.test.ts
# Pattern matching
pnpm test -- stores
# Single test by name
pnpm test -- -t "should create conversation"
Test Architecture
The project uses a three-layer testing strategy:
| Layer | Tool | Purpose |
|---|---|---|
| Unit | Vitest + React Testing Library | Component, hook, and store tests |
| Integration | Vitest | Store and service interaction tests |
| E2E | Playwright + Electron | Full application workflow tests |
Test Distribution
| Category | Files | Tests | Description |
|---|---|---|---|
| Store Tests | 5 | 243 | Zustand state management |
| Hook Tests | 1 | 29 | React hooks behavior |
| Service Tests | 2 | 87 | API clients and audio |
| Component Tests | 5 | 207 | React UI components |
| E2E Tests | 3 | 54 | Full app workflows |
| Total | 16 | 620 |
Configuration
Vitest (vitest.config.ts)
- Environment: happy-dom (fast DOM simulation)
- Coverage: v8 provider, 80% threshold
- Setup:
/src/test/setup.ts - Aliases:
@→src/renderer,@main→src/main
Playwright (playwright.config.ts)
- Test Directory:
/e2e - Reporters: HTML, JSON
- Timeout: 30s per test, 5s per assertion
- Features: Screenshots on failure, trace on retry
Test Setup (src/test/setup.ts)
Global mocks for:
- Window APIs:
matchMedia,localStorage,speechSynthesis,AudioContext - Electron APIs:
electronAPI,pathAPI,conversationAPI
Writing Tests
Store Test Pattern
import { describe, it, expect, beforeEach } from 'vitest';
import { useConversationStore } from '../conversationStore';
describe('conversationStore', () => {
beforeEach(() => {
// Reset store before each test
useConversationStore.setState({
conversations: new Map(),
activeConversationId: null,
tabOrder: [],
});
});
it('should create conversation', () => {
const { createConversation, getConversation } = useConversationStore.getState();
const id = createConversation('test-agent');
expect(getConversation(id)).toBeDefined();
expect(getConversation(id)?.agentId).toBe('test-agent');
});
});
Hook Test Pattern
import { renderHook } from '@testing-library/react';
import { useKeyboardShortcuts } from '../useKeyboardShortcuts';
it('should call callback on Ctrl+S', () => {
const onToggleSpeech = vi.fn();
renderHook(() => useKeyboardShortcuts({ onToggleSpeech }));
window.dispatchEvent(
new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true })
);
expect(onToggleSpeech).toHaveBeenCalled();
});
Component Test Pattern
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from './MyComponent';
// Mock stores
vi.mock('../../stores', () => ({
useMyStore: vi.fn(),
}));
describe('MyComponent', () => {
beforeEach(() => {
vi.clearAllMocks();
(useMyStore as any).mockReturnValue({
data: 'mock data',
action: vi.fn(),
});
});
it('handles user click', async () => {
const user = userEvent.setup();
render(<MyComponent />);
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(mockAction).toHaveBeenCalled();
});
});
E2E Test Pattern
import { test, expect } from './electron';
test('should launch app window', async ({ page }) => {
await expect(page.locator('[data-testid="app-layout"]')).toBeVisible();
await expect(page.locator('[data-testid="message-input"]')).toBeVisible();
});
Best Practices
Query Priority
Use accessible queries in this order:
// Preferred - accessible to users
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText(/username/i)
screen.getByText(/welcome/i)
// Fallback - for complex cases
screen.getByTestId('submit-button')
// Avoid - implementation details
container.querySelector('.submit-button')
Async Handling
// Elements that appear asynchronously
const element = await screen.findByText('Loaded');
// Assertions that need to wait
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
State Reset
Always reset stores to prevent state leakage:
beforeEach(() => {
vi.clearAllMocks();
useConversationStore.setState({ conversations: new Map() });
});
Debugging
Verbose Output
pnpm test -- --reporter=verbose
Print DOM Structure
it('debug example', () => {
render(<Component />);
console.log(screen.debug());
});
Run Failed Tests Only
pnpm test -- --changed
VS Code Debug Config
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["test", "--", "--run", "--no-coverage"],
"console": "integratedTerminal"
}
Common Issues
State Leaking Between Tests
Problem: Tests pass individually but fail together.
Solution: Reset stores in beforeEach:
beforeEach(() => {
useConversationStore.setState({ conversations: new Map() });
});
Mock Not Called
Problem: Expected function was never called.
Solution: Ensure mock is defined before import:
vi.mock('./module'); // Must be before import
import { Component } from './Component';
Async State Not Updated
Problem: Component state doesn't reflect changes.
Solution: Use waitFor:
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
Coverage
Generate Report
pnpm test:coverage
View at ./coverage/index.html
Thresholds
Configured in vitest.config.ts:
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 80% |
| Statements | 80% |
CI/CD
Tests run automatically on:
- Every push to main/develop
- Every pull request
- Manual workflow dispatch
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install
- run: pnpm test:run
- run: pnpm test:coverage
Test File Structure
src/
├── renderer/
│ ├── stores/__tests__/
│ │ ├── conversationStore.test.ts
│ │ ├── agentStore.test.ts
│ │ ├── uiStore.test.ts
│ │ ├── settingsStore.test.ts
│ │ └── voiceStore.test.ts
│ ├── hooks/__tests__/
│ │ └── useKeyboardShortcuts.test.ts
│ ├── services/__tests__/
│ │ ├── ChatterboxClient.test.ts
│ │ └── AudioPlaybackService.test.ts
│ └── components/
│ └── */*.test.tsx
├── main/
│ └── */__tests__/*.test.ts
└── test/
└── setup.ts
e2e/
├── app.e2e.ts
├── chat.e2e.ts
└── settings.e2e.ts