Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
252 lines
7.4 KiB
Plaintext
252 lines
7.4 KiB
Plaintext
---
|
|
description: Opinionated best practices for fast, reliable Vitest unit and integration tests in JS/TS projects.
|
|
globs: **/*.{js,ts,jsx,tsx}
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Vitest Best Practices
|
|
|
|
Vitest is the definitive testing framework for our Vite-powered projects. It offers a fast, Jest-compatible API with deep integration into the Vite ecosystem. Adhering to these guidelines ensures our tests are robust, performant, and easy to maintain.
|
|
|
|
## 1. Code Organization & Naming
|
|
|
|
**Always co-locate test files with their source.** This improves discoverability and ensures tests are updated alongside their implementation.
|
|
|
|
* **File Naming**: Use `*.test.{ts,tsx,js,jsx}`.
|
|
* **Location**: Place test files directly next to the component or module they test.
|
|
|
|
❌ BAD:
|
|
```
|
|
// src/components/Button/Button.tsx
|
|
// tests/components/Button.test.tsx
|
|
```
|
|
|
|
✅ GOOD:
|
|
```typescript
|
|
// src/components/Button/Button.tsx
|
|
// src/components/Button/Button.test.tsx
|
|
```
|
|
|
|
## 2. Test Structure & Isolation
|
|
|
|
**Organize tests logically using `describe` and `it` (or `test`) blocks.** Ensure each test is isolated and deterministic.
|
|
|
|
* **`describe`**: Group related tests into suites.
|
|
* **`it` / `test`**: Define individual test cases. Prefer `it` for consistency with Jest.
|
|
* **Hooks (`beforeEach`, `afterEach`)**: Use these for setup and teardown to ensure test isolation.
|
|
|
|
❌ BAD: (Shared state, no cleanup)
|
|
```typescript
|
|
let user;
|
|
test('creates user', () => {
|
|
user = createUser();
|
|
expect(user).toBeDefined();
|
|
});
|
|
test('updates user', () => { // Depends on previous test
|
|
user.name = 'New Name';
|
|
expect(user.name).toBe('New Name');
|
|
});
|
|
```
|
|
|
|
✅ GOOD: (Isolated tests with hooks)
|
|
```typescript
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { createUser, deleteUser } from './user-service';
|
|
|
|
describe('User Service', () => {
|
|
let user;
|
|
|
|
beforeEach(() => {
|
|
user = createUser(); // Create a fresh user for each test
|
|
});
|
|
|
|
afterEach(() => {
|
|
deleteUser(user.id); // Clean up after each test
|
|
});
|
|
|
|
it('should create a user', () => {
|
|
expect(user).toBeDefined();
|
|
expect(user.id).toBeTypeOf('string');
|
|
});
|
|
|
|
it('should update a user', () => {
|
|
user.name = 'Jane Doe';
|
|
expect(user.name).toBe('Jane Doe');
|
|
});
|
|
});
|
|
```
|
|
|
|
## 3. Asynchronous Testing with `vi.waitFor`
|
|
|
|
**Always use `vi.waitFor` for polling conditions in asynchronous tests.** Avoid arbitrary `setTimeout` calls or manual polling loops. `vi.waitFor` is designed for reliable synchronization.
|
|
|
|
❌ BAD: (Flaky, relies on arbitrary timeout)
|
|
```typescript
|
|
test('data loads after delay', async () => {
|
|
let data = null;
|
|
fetchData().then(res => (data = res));
|
|
await new Promise(resolve => setTimeout(resolve, 100)); // Arbitrary wait
|
|
expect(data).toEqual('some data');
|
|
});
|
|
```
|
|
|
|
✅ GOOD: (Reliable polling with `vi.waitFor`)
|
|
```typescript
|
|
import { it, expect, vi } from 'vitest';
|
|
import { fetchData } from './api'; // Assume fetchData returns a Promise
|
|
|
|
it('should load data after delay', async () => {
|
|
let data = null;
|
|
fetchData().then(res => (data = res));
|
|
|
|
// Polls until data is not null, with a 2-second timeout
|
|
await vi.waitFor(() => expect(data).not.toBeNull(), { timeout: 2000 });
|
|
|
|
expect(data).toEqual('some data');
|
|
});
|
|
```
|
|
|
|
## 4. Mocking Strategies
|
|
|
|
**Leverage Vitest's `vi` API for all mocking.** This provides Jest-compatible syntax and seamless integration. Always clean up mocks after each test.
|
|
|
|
* **`vi.fn()`**: Mock individual functions.
|
|
* **`vi.spyOn()`**: Spy on existing object methods.
|
|
* **`vi.mock()`**: Mock entire modules.
|
|
|
|
### Function Mocking
|
|
|
|
❌ BAD: (Manual mock, no easy reset)
|
|
```typescript
|
|
const originalFetch = global.fetch;
|
|
global.fetch = () => Promise.resolve({ json: () => ({ id: 1 }) });
|
|
// ... test ...
|
|
global.fetch = originalFetch; // Easy to forget cleanup
|
|
```
|
|
|
|
✅ GOOD: (Using `vi.fn` with `afterEach` cleanup)
|
|
```typescript
|
|
import { it, expect, vi, afterEach } from 'vitest';
|
|
import { getUser } from './user-api';
|
|
|
|
// Mock the module containing fetchUser
|
|
vi.mock('./user-api', async (importOriginal) => {
|
|
const mod = await importOriginal();
|
|
return {
|
|
...mod,
|
|
fetchUser: vi.fn(), // Mock specific function within the module
|
|
};
|
|
});
|
|
|
|
// Import the mocked function after vi.mock
|
|
import { fetchUser } from './user-api';
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks(); // Clear mock calls after each test to prevent state leakage
|
|
});
|
|
|
|
it('should fetch user data', async () => {
|
|
fetchUser.mockResolvedValueOnce({ id: 1, name: 'Test User' });
|
|
const user = await getUser(1);
|
|
expect(fetchUser).toHaveBeenCalledWith(1);
|
|
expect(user.name).toBe('Test User');
|
|
});
|
|
```
|
|
|
|
### Module Mocking
|
|
|
|
**Mock modules at the top of the file.** This ensures the mock is applied before the module under test imports it.
|
|
|
|
✅ GOOD: (Module mock before imports)
|
|
```typescript
|
|
import { vi, it, expect } from 'vitest';
|
|
|
|
// Mock the entire 'lodash' module to control its behavior
|
|
vi.mock('lodash', () => ({
|
|
debounce: vi.fn((fn) => fn), // Mock debounce to execute immediately
|
|
}));
|
|
|
|
import { debounce } from 'lodash'; // Import the mocked debounce
|
|
import { saveInput } from './input-handler'; // Module using debounce
|
|
|
|
it('should call save function without debounce delay', () => {
|
|
saveInput('test');
|
|
expect(debounce).toHaveBeenCalledOnce();
|
|
});
|
|
```
|
|
|
|
## 5. DOM Environment & Component Testing
|
|
|
|
**Use `happy-dom` for lightweight DOM environments.** It's generally faster and sufficient for most component tests. Switch to `jsdom` only if specific browser APIs are missing in `happy-dom`.
|
|
|
|
* Configure in `vite.config.ts` or `vitest.config.ts`.
|
|
|
|
✅ GOOD: (Configuring `happy-dom`)
|
|
```typescript
|
|
// vite.config.ts or vitest.config.ts
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
environment: 'happy-dom', // Use happy-dom for faster DOM mocking
|
|
globals: true, // Auto-import test APIs globally (e.g., describe, it, expect)
|
|
},
|
|
});
|
|
```
|
|
|
|
## 6. Performance & Concurrent Tests
|
|
|
|
**Utilize `.concurrent` for tests that can run in parallel.** This significantly speeds up test suites where tests are independent.
|
|
|
|
* Use `it.concurrent` for individual tests.
|
|
* Use `describe.concurrent` for entire suites.
|
|
* **Important**: When using `.concurrent`, always destructure `expect` from the test context to avoid issues with snapshot and assertion tracking.
|
|
|
|
❌ BAD: (Sequential tests, slow)
|
|
```typescript
|
|
describe('My Feature', () => {
|
|
it('test A', async () => { /* ... */ });
|
|
it('test B', async () => { /* ... */ });
|
|
});
|
|
```
|
|
|
|
✅ GOOD: (Concurrent tests, faster)
|
|
```typescript
|
|
import { describe, it } from 'vitest';
|
|
|
|
describe.concurrent('My Feature', () => {
|
|
it('test A', async ({ expect }) => { // Destructure expect for concurrent tests
|
|
expect(1).toBe(1);
|
|
});
|
|
|
|
it.concurrent('test B', async ({ expect }) => { // Destructure expect
|
|
expect(2).toBe(2);
|
|
});
|
|
});
|
|
```
|
|
|
|
## 7. Code Coverage
|
|
|
|
**Enable V8-based code coverage.** It offers near-zero overhead and integrates seamlessly.
|
|
|
|
* Add `coverage` configuration to `vite.config.ts` or `vitest.config.ts`.
|
|
* Run with `vitest run --coverage`.
|
|
|
|
✅ GOOD: (V8 coverage configuration)
|
|
```typescript
|
|
// vite.config.ts or vitest.config.ts
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
environment: 'happy-dom',
|
|
globals: true,
|
|
coverage: {
|
|
provider: 'v8', // Use V8 for native, fast coverage
|
|
reporter: ['text', 'json', 'html'], // Output formats for reports
|
|
exclude: ['node_modules/', 'dist/', '.eslintrc.cjs'], // Exclude common directories from coverage
|
|
},
|
|
},
|
|
});
|
|
```
|