Add comprehensive testing suite and fix QC issues

- Add 115+ unit, component, and E2E tests
- Add Vitest configuration with coverage thresholds
- Add validation schema tests (validations.test.ts)
- Add sanitizer utility tests (sanitizer.test.ts)
- Add WhatsApp service tests (whatsapp.test.ts)
- Add component tests for UnifiedTypography and UnifiedSurface
- Add E2E tests for admin auth and public pages
- Add testing documentation (docs/TESTING.md)
- Add sanitizer and WhatsApp utilities
- Add centralized validation schemas
- Refactor state management (admin/public separation)
- Fix security issues (OTP via POST, session password validation)
- Update AGENTS.md with testing guidelines

Test Coverage: 50%+ target achieved
All tests passing: 115/115

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-09 14:05:03 +08:00
parent 7bc546e985
commit 6ed2392420
46 changed files with 7734 additions and 565 deletions

380
docs/STATE_MANAGEMENT.md Normal file
View File

@@ -0,0 +1,380 @@
# State Management Guide
## Overview
Desa Darmasaba menggunakan **Valtio** untuk global state management. Valtio adalah state management library yang menggunakan proxy pattern untuk reactive state yang sederhana dan performant.
## Why Valtio?
-**Simple API** - Menggunakan plain JavaScript objects
-**Performant** - Component re-renders hanya saat state yang digunakan berubah
-**TypeScript-friendly** - Full TypeScript support
-**No boilerplate** - Tidak perlu actions, reducers, atau selectors
-**Flexible** - Bisa digunakan di dalam atau luar React components
## Installation
```bash
bun install valtio
```
## State Structure
```
src/state/
├── admin/ # Admin dashboard state
│ ├── index.ts # Admin state exports
│ ├── adminNavState.ts # Navigation state
│ ├── adminAuthState.ts # Authentication state
│ ├── adminFormState.ts # Form state (images, files)
│ └── adminModuleState.ts # Module-specific state
├── public/ # Public pages state
│ ├── index.ts # Public state exports
│ ├── publicNavState.ts # Navigation state
│ └── publicMusicState.ts # Music player state
├── darkModeStore.ts # Dark mode state (legacy)
└── index.ts # Main exports
```
## Basic Usage
### Creating State
```typescript
// src/state/example/exampleState.ts
import { proxy, useSnapshot } from 'valtio';
export const exampleState = proxy<{
count: number;
items: string[];
isLoading: boolean;
increment: () => void;
addItem: (item: string) => void;
}>({
count: 0,
items: [],
isLoading: false,
increment() {
exampleState.count += 1;
},
addItem(item: string) {
exampleState.items.push(item);
},
});
// Hook untuk React components
export const useExample = () => {
const snapshot = useSnapshot(exampleState);
return {
...snapshot,
increment: exampleState.increment,
addItem: exampleState.addItem,
};
};
```
### Using in React Components
```typescript
'use client';
import { useExample } from '@/state';
export function Counter() {
const { count, increment } = useExample();
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
```
### Using Outside React
```typescript
// In non-React code (utilities, services, etc.)
import { exampleState } from '@/state';
// Direct mutation
exampleState.count = 10;
exampleState.increment();
// Subscribe to changes
import { subscribe } from 'valtio';
subscribe(exampleState, () => {
console.log('State changed:', exampleState.count);
});
```
## Domain-Specific State
### Admin State
State untuk admin dashboard hanya digunakan di `/admin` routes.
```typescript
import {
adminNavState,
adminAuthState,
useAdminNav,
useAdminAuth
} from '@/state';
// In React component
export function AdminHeader() {
const { mobileOpen, toggleMobile } = useAdminNav();
const { user, isAuthenticated } = useAdminAuth();
return (
<Header>
<Button onClick={toggleMobile}>Menu</Button>
{user?.name}
</Header>
);
}
// Outside React
adminNavState.mobileOpen = true;
adminAuthState.clearUser();
```
### Public State
State untuk public pages hanya digunakan di `/darmasaba` routes.
```typescript
import {
publicNavState,
publicMusicState,
usePublicNav,
usePublicMusic
} from '@/state';
// In React component
export function MusicPlayer() {
const { isPlaying, currentSong, togglePlayPause } = usePublicMusic();
return (
<Player>
{currentSong?.judul}
<Button onClick={togglePlayPause}>
{isPlaying ? 'Pause' : 'Play'}
</Button>
</Player>
);
}
```
## Async Operations
```typescript
// src/state/example/dataState.ts
import { proxy, useSnapshot } from 'valtio';
import ApiFetch from '@/lib/api-fetch';
export const dataState = proxy<{
data: any[];
isLoading: boolean;
error: string | null;
fetchData: (id: string) => Promise<void>;
}>({
data: [],
isLoading: false,
error: null,
async fetchData(id: string) {
dataState.isLoading = true;
dataState.error = null;
try {
const response = await ApiFetch.someApi.get({ id });
dataState.data = response.data;
} catch (error) {
dataState.error = error instanceof Error ? error.message : 'Failed to fetch';
} finally {
dataState.isLoading = false;
}
},
});
export const useData = () => {
const snapshot = useSnapshot(dataState);
return {
...snapshot,
fetchData: dataState.fetchData,
};
};
```
## Best Practices
### ✅ DO
1. **Separate admin and public state**
```typescript
// Good
import { adminNavState } from '@/state/admin';
import { publicNavState } from '@/state/public';
```
2. **Use methods in state for complex operations**
```typescript
// Good
export const state = proxy({
count: 0,
increment() {
state.count += 1;
},
});
```
3. **Add error handling in async methods**
```typescript
// Good
async fetchData() {
state.isLoading = true;
state.error = null;
try {
// fetch logic
} catch (error) {
state.error = error.message;
} finally {
state.isLoading = false;
}
}
```
4. **Use TypeScript for type safety**
```typescript
// Good
type User = { id: string; name: string };
export const authState = proxy<{
user: User | null;
setUser: (user: User | null) => void;
}>({ ... });
```
### ❌ DON'T
1. **Don't mutate state directly in render**
```typescript
// Bad
function Component() {
state.count += 1; // Don't do this in render
return <div>{state.count}</div>;
}
```
2. **Don't mix admin and public state**
```typescript
// Bad
import { adminAuthState } from '@/state/admin';
import { publicNavState } from '@/state/public';
// Don't use admin state in public pages
```
3. **Don't create new objects in state methods**
```typescript
// Bad
increment() {
state.count = state.count + 1; // Creates new number
}
// Good
increment() {
state.count += 1; // Mutates existing value
}
```
## Migration from Legacy State
### Old Pattern (Deprecated)
```typescript
// Old pattern - still works but deprecated
import stateNav from '@/state/state-nav';
import { authStore } from '@/store/authStore';
```
### New Pattern (Recommended)
```typescript
// New pattern - recommended
import { adminNavState } from '@/state/admin';
import { adminAuthState } from '@/state/admin';
```
## Music Player State
Music player sekarang menggunakan Valtio state dengan React Context wrapper untuk backward compatibility.
```typescript
// New way (recommended)
import { usePublicMusic } from '@/state/public';
function MusicPlayer() {
const { isPlaying, currentSong, togglePlayPause } = usePublicMusic();
// ...
}
// Old way (still works for backward compatibility)
import { useMusic } from '@/app/context/MusicContext';
function MusicPlayer() {
const { isPlaying, currentSong, togglePlayPause } = useMusic();
// ...
}
```
## Troubleshooting
### State not updating in component
Make sure you're using the hook in component:
```typescript
// Good
function Component() {
const { count } = useExample(); // Subscribe to state
return <div>{count}</div>;
}
// Bad
function Component() {
const count = exampleState.count; // No subscription
return <div>{count}</div>;
}
```
### Performance issues
Use selective subscriptions:
```typescript
// Good - only subscribe to what you need
function Component() {
const { count } = useExample(); // Only count
return <div>{count}</div>;
}
// Bad - subscribe to entire state
function Component() {
const state = useExample(); // Entire state
return <div>{state.count}</div>;
}
```
## Additional Resources
- [Valtio Documentation](https://github.com/pmndrs/valtio)
- [Valtio Examples](https://github.com/pmndrs/valtio/tree/main/examples)
- [Reactivity Guide](https://docs.pmnd.rs/valtio/guides/reactivity)

540
docs/TESTING.md Normal file
View File

@@ -0,0 +1,540 @@
# Testing Guide - Desa Darmasaba
## Overview
This document provides comprehensive testing guidelines for the Desa Darmasaba project. The project uses a multi-layered testing strategy including unit tests, component tests, and end-to-end (E2E) tests.
## Testing Stack
| Layer | Tool | Purpose |
|-------|------|---------|
| **Unit Tests** | Vitest | Testing utility functions, validation schemas, services |
| **Component Tests** | Vitest + React Testing Library | Testing React components in isolation |
| **E2E Tests** | Playwright | Testing complete user flows in real browsers |
| **API Mocking** | MSW (Mock Service Worker) | Mocking API responses for unit/component tests |
## Test Structure
```
__tests__/
├── api/ # API integration tests
│ └── fileStorage.test.ts
├── components/ # Component tests
│ └── admin/
│ ├── UnifiedTypography.test.tsx
│ └── UnifiedSurface.test.tsx
├── e2e/ # End-to-end tests
│ ├── admin/
│ │ └── auth.spec.ts
│ └── public/
│ └── pages.spec.ts
├── lib/ # Unit tests for utilities
│ ├── validations.test.ts
│ ├── sanitizer.test.ts
│ └── whatsapp.test.ts
├── mocks/ # MSW mocks for API
│ ├── handlers.ts
│ └── server.ts
└── setup.ts # Test setup and configuration
```
## Running Tests
### All Tests
```bash
bun run test
```
### Unit Tests Only
```bash
bun run test:api
```
### E2E Tests Only
```bash
bun run test:e2e
```
### Tests with Coverage
```bash
bun run test:api --coverage
```
### Run Specific Test File
```bash
bunx vitest run __tests__/lib/validations.test.ts
```
### Run Tests in Watch Mode
```bash
bunx vitest
```
### Run E2E Tests with UI
```bash
bun run test:e2e --ui
```
## Writing Tests
### Unit Tests (Vitest)
Unit tests should test pure functions, validation schemas, and utilities in isolation.
```typescript
// __tests__/lib/example.test.ts
import { describe, it, expect } from 'vitest';
import { exampleFunction } from '@/lib/example';
describe('exampleFunction', () => {
it('should return expected value for valid input', () => {
const result = exampleFunction('valid-input');
expect(result).toBe('expected-output');
});
it('should handle edge cases', () => {
expect(() => exampleFunction('')).toThrow();
expect(() => exampleFunction(null)).toThrow();
});
});
```
### Component Tests (React Testing Library)
Component tests should test React components in isolation with mocked dependencies.
```typescript
// __tests__/components/Example.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider, createTheme } from '@mantine/core';
import { ExampleComponent } from '@/components/Example';
function renderWithMantine(ui: React.ReactElement) {
const theme = createTheme();
return render(ui, {
wrapper: ({ children }) => (
<MantineProvider theme={theme}>{children}</MantineProvider>
),
});
}
describe('ExampleComponent', () => {
it('should render with props', () => {
renderWithMantine(<ExampleComponent title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('should handle user interactions', async () => {
const onClick = vi.fn();
renderWithMantine(<ExampleComponent onClick={onClick} />);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
```
### E2E Tests (Playwright)
E2E tests should test complete user flows in a real browser environment.
```typescript
// __tests__/e2e/example.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
// Setup before each test
await page.goto('/starting-page');
});
test('should complete user flow', async ({ page }) => {
// Fill form
await page.fill('input[name="email"]', 'user@example.com');
await page.click('button[type="submit"]');
// Wait for navigation
await page.waitForURL('/success');
// Verify result
await expect(page.getByText('Success!')).toBeVisible();
});
test('should handle errors gracefully', async ({ page }) => {
// Submit invalid data
await page.click('button[type="submit"]');
// Verify error message
await expect(page.getByText('Validation error')).toBeVisible();
});
});
```
### API Mocking (MSW)
Use MSW to mock API responses for unit and component tests.
```typescript
// __tests__/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/example', () => {
return HttpResponse.json({
data: [{ id: '1', name: 'Item 1' }],
});
}),
http.post('/api/example', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({
data: { id: '2', ...body },
status: 201,
});
}),
];
```
## Test Coverage Goals
Current coverage thresholds (configured in `vitest.config.ts`):
| Metric | Target |
|--------|--------|
| Branches | 50% |
| Functions | 50% |
| Lines | 50% |
| Statements | 50% |
### Critical Files Priority
Focus testing efforts on these critical files first:
1. **Validation & Security**
- `src/lib/validations/index.ts`
- `src/lib/sanitizer.ts`
- `src/lib/whatsapp.ts`
- `src/lib/session.ts`
2. **Core Utilities**
- `src/lib/api-fetch.ts`
- `src/lib/prisma.ts`
- `src/utils/themeTokens.ts`
3. **Shared Components**
- `src/components/admin/UnifiedTypography.tsx`
- `src/components/admin/UnifiedSurface.tsx`
- `src/components/admin/UnifiedCard.tsx`
4. **State Management**
- `src/state/darkModeStore.ts`
- `src/state/admin/*.ts`
- `src/state/public/*.ts`
5. **API Routes**
- `src/app/api/[[...slugs]]/_lib/auth/**`
- `src/app/api/[[...slugs]]/_lib/desa/**`
## Testing Conventions
### Naming Conventions
- **Unit/Component Tests**: `*.test.ts` or `*.test.tsx`
- **E2E Tests**: `*.spec.ts`
- **Test Files**: Match source file name (e.g., `sanitizer.ts``sanitizer.test.ts`)
- **Test Directories**: Mirror source structure under `__tests__/`
### Describe Blocks
Use nested `describe` blocks to organize tests logically:
```typescript
describe('FeatureName', () => {
describe('functionName', () => {
describe('when valid input', () => {
it('should return expected result', () => {});
});
describe('when invalid input', () => {
it('should throw error', () => {});
});
});
});
```
### Test Descriptions
- Use clear, descriptive test names
- Follow pattern: `should [expected behavior] when [condition]`
- Avoid vague descriptions like "works correctly"
### Assertions
- Use specific matchers (`toBe`, `toEqual`, `toContain`)
- Test both success and failure cases
- Test edge cases (empty input, null, undefined, max values)
### Setup and Teardown
```typescript
describe('ComponentName', () => {
beforeEach(() => {
// Reset mocks, state
vi.clearAllMocks();
});
afterEach(() => {
// Cleanup
vi.restoreAllMocks();
});
// ... tests
});
```
## Mocking Guidelines
### Mock External Services
```typescript
// Mock fetch API
global.fetch = vi.fn();
// Mock modules
vi.mock('@/lib/prisma', () => ({
default: {
berita: {
findMany: vi.fn(),
create: vi.fn(),
},
},
}));
```
### Mock Environment Variables
```typescript
const originalEnv = process.env;
beforeEach(() => {
process.env = {
...originalEnv,
TEST_VAR: 'test-value',
};
});
afterEach(() => {
process.env = originalEnv;
});
```
### Mock Date/Time
```typescript
const mockDate = new Date('2024-01-01T00:00:00Z');
vi.useFakeTimers();
vi.setSystemTime(mockDate);
// ... tests
vi.useRealTimers();
```
## E2E Testing Best Practices
### Test User Flows, Not Implementation
✅ Good:
```typescript
test('user can login and view dashboard', async ({ page }) => {
await page.goto('/admin/login');
await page.fill('input[name="nomor"]', '08123456789');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/admin/dashboard');
});
```
❌ Bad:
```typescript
test('login form submits to API', async ({ page }) => {
// Don't test internal implementation details
});
```
### Use Data Attributes for Selectors
```typescript
// In component
<button data-testid="submit-button">Submit</button>
// In test
await page.getByTestId('submit-button').click();
```
### Handle Async Operations
```typescript
// Wait for specific element
await page.waitForSelector('.loaded-content');
// Wait for navigation
await page.waitForNavigation();
// Wait for network request
await page.waitForResponse('/api/data');
```
### Skip Tests Appropriately
```typescript
// Skip in CI
test.skip(process.env.CI === 'true', 'Skip in CI environment');
// Skip with reason
test.skip(true, 'Feature not yet implemented');
// Conditional skip
test.skip(!hasValidCredentials, 'Requires valid credentials');
```
## Continuous Integration
### GitHub Actions Workflow
Tests run automatically on:
- Pull requests
- Push to main branch
- Manual trigger
### Test Requirements
- All new features must include tests
- Bug fixes should include regression tests
- Coverage should not decrease significantly
## Debugging Tests
### Vitest Debug Mode
```bash
bunx vitest --reporter=verbose
```
### Playwright Debug Mode
```bash
PWDEBUG=1 bun run test:e2e
```
### Playwright Trace Viewer
```bash
bun run test:e2e --trace on
bunx playwright show-trace
```
## Common Patterns
### Testing Validation Schemas
```typescript
describe('validationSchema', () => {
it('should accept valid data', () => {
const result = validationSchema.safeParse(validData);
expect(result.success).toBe(true);
});
it('should reject invalid data', () => {
const result = validationSchema.safeParse(invalidData);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toContain('error message');
}
});
});
```
### Testing Async Functions
```typescript
it('should fetch data successfully', async () => {
const result = await fetchData();
expect(result).toEqual(expectedData);
});
it('should handle errors', async () => {
await expect(asyncFunction()).rejects.toThrow('error message');
});
```
### Testing Hooks
```typescript
import { renderHook, act } from '@testing-library/react';
it('should update state', () => {
const { result } = renderHook(() => useCustomHook());
act(() => {
result.current.setValue('new value');
});
expect(result.current.value).toBe('new value');
});
```
## Troubleshooting
### Common Issues
**Issue**: Tests fail with "Cannot find module"
**Solution**: Check import paths, ensure `@/` alias is configured in `vitest.config.ts`
**Issue**: Mantine components throw errors
**Solution**: Wrap components with `MantineProvider` in test setup
**Issue**: Tests fail in CI but pass locally
**Solution**: Check for environment-specific code, use proper mocking
**Issue**: E2E tests timeout
**Solution**: Increase timeout, check for async operations, use proper waits
### Getting Help
- Check existing tests for patterns
- Review Vitest documentation: https://vitest.dev
- Review Playwright documentation: https://playwright.dev
- Review Testing Library documentation: https://testing-library.com
## Resources
- [Vitest Documentation](https://vitest.dev)
- [Playwright Documentation](https://playwright.dev)
- [React Testing Library](https://testing-library.com/react)
- [MSW Documentation](https://mswjs.io)
- [Testing JavaScript Course](https://testingjavascript.com)
## Maintenance
### Regular Tasks
- [ ] Update test dependencies monthly
- [ ] Review and update test coverage goals quarterly
- [ ] Remove deprecated test patterns
- [ ] Add tests for newly discovered edge cases
- [ ] Document common testing patterns
### Deprecation Policy
When refactoring code:
1. Keep existing tests passing
2. Update tests to match new implementation
3. Remove tests for removed functionality
4. Update this documentation
---
**Last Updated**: March 9, 2026
**Version**: 1.0.0
**Maintained By**: Development Team