Frontend Testing Guide¶
Overview¶
The HomePot frontend uses Vitest as the testing framework, providing fast, modern unit and integration testing for React components.
Testing Stack¶
Core Testing Tools¶
- Vitest (3.2.4) - Fast unit test framework built on Vite
- @testing-library/react (16.3.0) - React component testing utilities
- @testing-library/jest-dom (6.9.1) - Custom DOM matchers
- jsdom (25.0.0) - DOM environment for Node.js
- @vitest/ui (3.2.4) - Interactive web-based test UI
- @vitest/coverage-v8 (3.2.4) - Code coverage reporting
Available Commands¶
# Run all tests once
npm run test
# Run tests in watch mode (auto-rerun on file changes)
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Open interactive test UI in browser
npm run test -- --ui
Test Structure¶
frontend/tests/
├── setup.js # Global test configuration
├── unit/ # Unit tests for components
│ └── example.test.jsx # Example component tests
└── integration/ # Integration tests
└── example.test.jsx # Example integration tests
Writing Tests¶
Basic Test Structure¶
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from '@/components/ui/button';
describe('Button Component', () => {
it('renders button with text', () => {
render(<Button>Click Me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click Me');
});
it('handles click events', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByRole('button');
button.click();
expect(handleClick).toHaveBeenCalledOnce();
});
});
Common Testing Patterns¶
Testing Component Rendering¶
it('renders component with props', () => {
render(<MyComponent title="Test" />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
Testing User Interactions¶
import { userEvent } from '@testing-library/user-event';
it('handles user input', async () => {
const user = userEvent.setup();
render(<SearchBox />);
const input = screen.getByRole('textbox');
await user.type(input, 'search query');
expect(input).toHaveValue('search query');
});
Testing Async Operations¶
it('loads data asynchronously', async () => {
render(<DataComponent />);
// Wait for element to appear
const data = await screen.findByText('Loaded Data');
expect(data).toBeInTheDocument();
});
Testing Router Components¶
import { BrowserRouter } from 'react-router-dom';
it('renders route component', () => {
render(
<BrowserRouter>
<MyRoutedComponent />
</BrowserRouter>
);
expect(screen.getByText('Page Content')).toBeInTheDocument();
});
Coverage Configuration¶
Coverage is configured in vitest.config.js:
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'*.config.js',
'dist/',
],
}
Viewing Coverage Reports¶
After running npm run test:coverage, open:
Coverage Goals¶
- Overall Coverage: Target 80%+ for critical paths
- Components: 90%+ for reusable UI components
- Utils: 100% for utility functions
- Pages: 70%+ for page components
CI/CD Integration¶
Tests run automatically in GitHub Actions on: - Every pull request - Every push to main or develop - Node.js 22.x
Workflow Steps¶
- Install dependencies (with caching)
- Run ESLint
- Build production bundle
- Run tests (fails PR if tests fail)
- Upload build artifacts
Current Test Results¶
Best Practices¶
1. Test Behavior, Not Implementation¶
Don't test:
Do test:
2. Use Semantic Queries¶
Priority order: 1. getByRole - Most accessible 2. getByLabelText - Form elements 3. getByPlaceholderText - Input placeholders 4. getByText - Content 5. getByTestId - Last resort
3. Test Accessibility¶
it('is keyboard accessible', () => {
render(<Button>Submit</Button>);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveFocus();
});
4. Mock External Dependencies¶
import { vi } from 'vitest';
vi.mock('@/services/api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'test' }))
}));
5. Clean Up After Tests¶
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Common Issues & Solutions¶
Issue: Tests fail with "Cannot find module"¶
Solution: Check path aliases in vitest.config.js:
Issue: Router tests fail¶
Solution: Wrap component in Router:
import { BrowserRouter } from 'react-router-dom';
render(
<BrowserRouter>
<Component />
</BrowserRouter>
);
Issue: Async tests timeout¶
Solution: Increase timeout:
Adding Tests for New Components¶
-
Create test file next to component:
-
Write tests for:
- Rendering with different props
- User interactions
- Edge cases
-
Accessibility
-
Run tests locally:
-
Check coverage:
-
Commit when all tests pass
Test Examples¶
Testing Dashboard Component¶
describe('Dashboard', () => {
it('displays CPU usage chart', () => {
render(<Dashboard />);
expect(screen.getByText('CPU Usage')).toBeInTheDocument();
});
it('shows heartbeat indicators', () => {
render(<Dashboard />);
const heartbeats = screen.getAllByRole('status');
expect(heartbeats).toHaveLength(12);
});
});
Testing Form Components¶
describe('LoginForm', () => {
it('submits with valid credentials', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(
screen.getByLabelText('Email'),
'user@example.com'
);
await userEvent.type(
screen.getByLabelText('Password'),
'password123'
);
await userEvent.click(screen.getByRole('button', { name: 'Log In' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});
Resources¶
Next Steps¶
- Add tests for existing components:
- Button component (started)
- Card component
- Dashboard page
- Login page
-
Site management pages
-
Set up pre-commit hooks to run tests
-
Add E2E tests with Playwright
-
Configure coverage thresholds
Status: Testing infrastructure complete and passing in CI/CD
Last Updated: October 18, 2025