Update test:unit script to use cross-env for ELECTRON_RUN_AS_NODE, ensuring child processes inherit the variable and preventing resource leaks. Remove manual process.env setting from vitest.config.ts and add documentation on handling related errors in Testing.md. Also, add 'hanging-process' reporter to Vitest config for improved diagnostics.
7.2 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__/__mocks__/ directory, and import them in src/__tests__/setup-vitest.ts:
- Services from window APIs (
window.service,window.remote,window.observables) and container APIs (@services/container) are now mocked insrc/__tests__/__mocks__/window.ts和services-container.ts - Common libraries (
react-i18nextinreact-i18next.ts, logger inservices-log.ts)
Most of services should be in these mock files. Only mock specific small set of service API in new test files if needed.
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.
Errors
close timed out after 10000ms / FILEHANDLE (unknown stack trace)
This happens because Electron/Vitest child processes do not inherit the ELECTRON_RUN_AS_NODE environment variable, so resources cannot be cleaned up and handles leak.
Do not set ELECTRON_RUN_AS_NODE in vitest.config.ts via process.env.ELECTRON_RUN_AS_NODE = 'true' — this only affects the main process, not child processes.
Always use cross-env in your test script. For example:
cross-env ELECTRON_RUN_AS_NODE=1 pnpm exec ./node_modules/.bin/electron ./node_modules/vitest/vitest.mjs run
Or run manually in shell: $env:ELECTRON_RUN_AS_NODE=1; pnpm run test:unit
We use ELECTRON_RUN_AS_NODE to solve native modules (like better-sqlite3) being compiled for the wrong Node.js version, see the section in ErrorDuringStart.md.