desktop-chat-app/docs/TESTING.md
Lilith 779960fa63 docs: update documentation for HTTP service architecture
- 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>
2025-12-28 17:03:49 -08:00

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, @mainsrc/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

Resources