TidGi-Desktop/docs/Testing.md
2025-06-19 17:22:24 +08:00

5.9 KiB

Testing Guide

Testing guide for TidGi-Desktop using Vitest + React Testing Library for unit tests and Playwright + Cucumber for E2E tests.

Quick Start

# Run all tests
pnpm test

# Run unit tests only
pnpm test:unit

# Run E2E tests (requires packaged app)
pnpm run package:dev && pnpm test:e2e

# Run with coverage
pnpm test:unit -- --coverage

# Run a test file you newly written
pnpm test:unit src/services/agentDefinition/__tests__/responsePatternUtility.test.ts

Project Setup

Test Configuration: TypeScript-first with vitest.config.ts

  • Unit tests: Vitest + React Testing Library + jsdom
  • E2E tests: Playwright + Cucumber
  • Coverage: HTML reports in coverage/

File Structure:

src/
├── __tests__/           # Global test setup & utilities
├── components/*/
│   └── __tests__/       # Component tests
└── services/*/
    └── __tests__/       # Service tests

features/                # E2E tests
├── *.feature           # Gherkin scenarios
├── stepDefinitions/    # Playwright implementations
└── supports/           # Test utilities

Writing Unit Tests

Component Testing Best Practices

// Use semantic queries and user-event for realistic interactions
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('WorkspaceSelector', () => {
  it('should switch to Help page when clicking Help workspace', async () => {
    const user = userEvent.setup();
    render(<WorkspaceSelector />);
    
    // Wait for async content to load
    expect(await screen.findByText('Guide Page Content')).toBeInTheDocument();
    
    // Use realistic user interactions
    const helpText = await screen.findByText('Help');
    await user.click(helpText);
    
    // Assert on user-visible changes
    expect(await screen.findByText('Help Page Content')).toBeInTheDocument();
  });
});

Effective Mocking

// Mock complex components simply
vi.mock('../ComplexComponent', () => ({
  default: () => <div data-testid="complex-component">Mocked Component</div>,
}));

// Test-specific data for current test file
const workspacesSubject = new BehaviorSubject([
  { id: 'test-workspace', name: 'Test Wiki' }
]);

// Override global observables for this test
Object.defineProperty(window.observables.workspace, 'workspaces$', {
  value: workspacesSubject.asObservable(),
  writable: true,
});

Global Mock Management

Centralize common mocks in src/__tests__/setup-vitest.ts:

  • Window APIs (window.service, window.remote, window.observables)
  • Electron APIs (ipcRenderer, shell)
  • Common libraries (react-i18next)

Override in test files only when you need test-specific data:

// Only override what's specific to this test
Object.defineProperty(window.observables.workspace, 'workspaces$', {
  value: testSpecificWorkspaces$.asObservable(),
  writable: true,
});

This keeps tests focused and reduces duplication across test files.

Async Testing Patterns

// Use findBy* for elements that appear asynchronously
expect(await screen.findByText('Loading complete')).toBeInTheDocument();

// Use waitForElementToBeRemoved for disappearing elements
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));

// Avoid unnecessary waitFor - prefer findBy*
// ❌ Don't do this
await waitFor(() => {
  expect(screen.getByText('Content')).toBeInTheDocument();
});

// ✅ Do this instead
expect(await screen.findByText('Content')).toBeInTheDocument();

Writing E2E Tests

Feature File Example

# features/workspace-management.feature
Feature: Workspace Management
  
  Scenario: Create new workspace
    Given TidGi application is launched
    When I click "Add Workspace" button
    And I enter workspace name "Test Wiki"
    And I click "Create" button
    Then I should see "Test Wiki" in workspace list

Step Definitions

// features/stepDefinitions/application.ts
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { ElectronApplication, _electron as electron } from 'playwright';

let electronApp: ElectronApplication;
let page: Page;

Before(async function () {
  electronApp = await electron.launch({
    args: [path.join(__dirname, '../../out/TidGi-Desktop/TidGi-Desktop.exe')],
    timeout: 30000,
  });
  page = await electronApp.firstWindow();
});

Given('TidGi application is launched', async function () {
  await page.waitForSelector('[data-testid="main-window"]');
});

When('I click {string} button', async function (buttonText: string) {
  await page.click(`button:has-text("${buttonText}")`);
});

Then('I should see {string} in workspace list', async function (text: string) {
  await expect(page.locator(`[data-testid="workspace-item"]:has-text("${text}")`))
    .toBeVisible();
});

Testing Library Best Practices

Query Priority (use in this order)

  1. Accessible queries - getByRole, getByLabelText, getByPlaceholderText
  2. Semantic queries - getByAltText, getByTitle
  3. Test IDs - getByTestId (when accessibility queries aren't practical)

Async Patterns

  • Use findBy* instead of getBy* + waitFor
  • Use user-event instead of fireEvent for realistic interactions
  • Wait for initial async state in beforeEach to avoid act() warnings

Common Antipatterns to Avoid

// ❌ Testing implementation details
expect(component.state.isLoading).toBe(false);

// ✅ Testing user-visible behavior  
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();

// ❌ Using act() wrapper unnecessarily
act(() => {
  fireEvent.click(button);
});

// ✅ Using proper async testing
const user = userEvent.setup();
await user.click(button);

For complete Testing Library guidance, see Testing Library docs.