mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-15 05:42:17 -08:00
5.8 KiB
5.8 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
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)
- Accessible queries -
getByRole,getByLabelText,getByPlaceholderText - Semantic queries -
getByAltText,getByTitle - Test IDs -
getByTestId(when accessibility queries aren't practical)
Async Patterns
- Use
findBy*instead ofgetBy*+waitFor - Use
user-eventinstead offireEventfor realistic interactions - Wait for initial async state in
beforeEachto 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.