TidGi-Desktop/docs/Testing.md
2025-06-13 13:56:30 +08:00

6.2 KiB

Testing Guide

Comprehensive testing guide for TidGi-Desktop using Jest + 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

Project Setup

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

  • Unit tests: Jest + 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

Basic Component Test

// src/components/Button/__tests__/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '../Button';

describe('Button', () => {
  it('should handle click events', () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Click me</Button>);
    
    fireEvent.click(screen.getByText('Click me'));
    
    expect(onClick).toHaveBeenCalledTimes(1);
  });
});

Testing with Material-UI

import { ThemeProvider, createTheme } from '@mui/material/styles';

const renderWithTheme = (component: React.ReactElement) => {
  return render(
    <ThemeProvider theme={createTheme()}>
      {component}
    </ThemeProvider>
  );
};

Async & API Tests

// Mock fetch globally
global.fetch = vi.fn();

beforeEach(() => {
  vi.clearAllMocks();
});

it('should fetch data successfully', async () => {
  const mockData = { id: 1, name: 'Test' };
  
  (fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
    ok: true,
    json: async () => mockData,
  } as Response);

  const result = await fetchUserData(1);
  
  expect(fetch).toHaveBeenCalledWith('/api/users/1');
  expect(result).toEqual(mockData);
});

Testing Electron IPC

// Mock electron
vi.mock('electron', () => ({
  ipcRenderer: {
    invoke: vi.fn(),
    send: vi.fn(),
    on: vi.fn(),
  },
}));

import { ipcRenderer } from 'electron';

it('should communicate with main process', async () => {
  const mockResponse = { success: true };
  (ipcRenderer.invoke as jest.MockedFunction<typeof ipcRenderer.invoke>)
    .mockResolvedValueOnce(mockResponse);

  const result = await electronService.getData();
  
  expect(ipcRenderer.invoke).toHaveBeenCalledWith('get-data');
  expect(result).toEqual(mockResponse);
});

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();
});

Best Practices

Unit Testing

  • Test behavior, not implementation - Focus on user interactions and outputs
  • Use data-testid for reliable element selection
  • Mock external dependencies - APIs, file system, Electron APIs
  • Test error cases - Don't just test the happy path

E2E Testing

  • Use semantic selectors - data-testid, role, or text content
  • Wait for elements - Use waitFor, findBy queries
  • Test real workflows - Complete user scenarios, not isolated features
  • Keep scenarios focused - One feature per scenario

General

  • Arrange-Act-Assert pattern for clear test structure
  • Descriptive test names - What should happen, when, under what conditions
  • Clean up between tests - Reset mocks, clear state

Debugging

# Debug unit tests
pnpm test:unit -- --runInBand src/path/to/test.ts

# Debug E2E tests (headed mode)
pnpm test:e2e -- --headed

# Use screen.debug() to see DOM state
screen.debug(); // In React tests

# Check coverage gaps
pnpm test:unit -- --coverage
# Open coverage/lcov-report/index.html

Common Issues

Issue Solution
Tests timeout Check for unresolved promises, increase timeout
Mock not working Ensure mocks are set up before imports
CSS/Asset errors Check moduleNameMapper in jest.config.ts
E2E test fails Verify app is packaged, check selectors

CI Integration

Tests run automatically on PRs and main branch pushes. Ensure all tests pass:

pnpm test        # Full test suite
pnpm lint:fix    # Fix linting issues

References