Phase 13: Admin Dashboard - Testing Strategy¶
Test Pyramid¶
Unit Tests (Vitest)¶
What to Test¶
- React Components: Rendering, props, user interactions
- Hooks: Custom hooks (API calls, state management)
- Utilities: Helper functions, formatters, validators
Example: Component Test¶
// features/database/components/TableBrowser.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TableBrowser } from './TableBrowser';
describe('TableBrowser', () => {
it('renders table data', async () => {
const queryClient = new QueryClient();
render(
<QueryClientProvider client={queryClient}>
<TableBrowser collection="users" />
</QueryClientProvider>
);
await waitFor(() => {
expect(screen.getByText('users')).toBeInTheDocument();
});
});
it('handles empty state', () => {
render(<TableBrowser collection="empty_table" />);
expect(screen.getByText('No data')).toBeInTheDocument();
});
});
Example: Hook Test¶
// features/database/hooks/useTableData.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useTableData } from './useTableData';
describe('useTableData', () => {
it('fetches table data', async () => {
const queryClient = new QueryClient();
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useTableData('users', { limit: 10, offset: 0 }), { wrapper });
await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});
});
Integration Tests (React Testing Library)¶
What to Test¶
- Feature flows: Multi-step user journeys
- API integration: Components + API client
- State management: React Query + Zustand integration
Example: Login Flow¶
// features/auth/LoginPage.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { LoginPage } from './LoginPage';
const server = setupServer(
rest.post('/auth/login', (req, res, ctx) => {
return res(ctx.json({ access_token: 'fake-token' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('LoginPage', () => {
it('logs in successfully', async () => {
render(<LoginPage />);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' },
});
fireEvent.click(screen.getByText('Login'));
await waitFor(() => {
expect(localStorage.getItem('access_token')).toBe('fake-token');
});
});
});
E2E Tests (Playwright)¶
What to Test¶
- Critical paths: Login → browse data → logout
- Cross-page flows: Create user → assign role → verify permissions
- Visual regression: Screenshots of key pages
Example: Table Browsing Flow¶
// tests/e2e/table-browsing.spec.ts
import { test, expect } from '@playwright/test';
test('browse table data', async ({ page }) => {
// Login
await page.goto('http://localhost:5173/login');
await page.fill('input[name="email"]', 'admin@example.com');
await page.fill('input[name="password"]', 'admin123');
await page.click('button[type="submit"]');
// Navigate to database section
await page.click('text=Database');
await page.waitForURL('**/database/tables');
// Select a table
await page.click('text=users');
// Verify table data loaded
await expect(page.locator('table')).toBeVisible();
await expect(page.locator('tbody tr')).toHaveCount(20); // First page
// Test pagination
await page.click('text=Next');
await expect(page.locator('tbody tr')).toHaveCount(20); // Second page
// Test filtering
await page.fill('input[placeholder="Search"]', 'john');
await page.click('text=Apply Filter');
await expect(page.locator('tbody tr')).toHaveCount.toBeLessThan(20);
// Screenshot for visual regression
await page.screenshot({ path: 'screenshots/table-browser.png' });
});
Example: Real-Time Subscription¶
// tests/e2e/realtime.spec.ts
import { test, expect } from '@playwright/test';
test('subscribe to real-time events', async ({ page }) => {
await page.goto('http://localhost:5173/realtime/events');
// Enable live updates
await page.click('text=Enable Live Updates');
// Verify WebSocket connection
await expect(page.locator('text=Connected')).toBeVisible();
// In another tab, insert data (simulate)
await page.evaluate(() => {
fetch('http://localhost:54321/rest/v1/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer token' },
body: JSON.stringify({ title: 'New Post' }),
});
});
// Verify event received
await expect(page.locator('text=INSERT: posts')).toBeVisible({ timeout: 5000 });
});
Visual Regression Testing¶
Use Playwright's screenshot comparison:
// tests/e2e/visual.spec.ts
import { test, expect } from '@playwright/test';
test('homepage visual regression', async ({ page }) => {
await page.goto('http://localhost:5173');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
});
});
API Mocking (MSW)¶
Use Mock Service Worker for deterministic tests:
// src/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/rest/v1/users', (req, res, ctx) => {
return res(
ctx.json({
data: [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
],
count: 2,
limit: 20,
offset: 0,
})
);
}),
rest.post('/auth/login', (req, res, ctx) => {
const { email, password } = req.body as any;
if (email === 'admin@example.com' && password === 'admin123') {
return res(ctx.json({ access_token: 'mock-token' }));
}
return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
}),
];
// src/mocks/browser.ts
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Start in dev mode
if (import.meta.env.DEV) {
worker.start();
}
Coverage Targets¶
- Unit tests: 80% line coverage
- Integration tests: 70% feature coverage
- E2E tests: 100% critical path coverage
Run coverage:
CI/CD Pipeline¶
GitHub Actions¶
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:unit
- run: npm run test:coverage
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install
- run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: tests/e2e/screenshots/
Performance Testing¶
Lighthouse CI¶
# .github/workflows/lighthouse.yml
name: Lighthouse
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:5173
http://localhost:5173/database
uploadArtifacts: true
temporaryPublicStorage: true
Targets: - Performance score: > 90 - Accessibility score: 100 - Best Practices score: > 95
Test Data Management¶
Fixtures¶
// tests/fixtures/users.ts
export const mockUsers = [
{ id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin' },
{ id: '2', name: 'Bob', email: 'bob@example.com', role: 'user' },
];
// tests/fixtures/tables.ts
export const mockTables = [
{ name: 'users', count: 100 },
{ name: 'posts', count: 500 },
];
Testing Best Practices¶
- Arrange-Act-Assert: Structure tests clearly
- Test behavior, not implementation: Avoid testing internal state
- Use data-testid sparingly: Prefer accessible queries (getByRole, getByLabelText)
- Avoid flaky tests: Use
waitFor, avoid hardcoded timeouts - Keep tests fast: Mock external dependencies