| name | testing-frontend |
| version | 1.0.0 |
| description | Use when writing component tests, testing user interactions, mocking APIs, or setting up Vitest/React Testing Library/Vue Test Utils for frontend applications. |
| keywords | ["frontend testing","Vitest","React Testing Library","Vue Test Utils","component testing","user event testing","mocking","accessibility testing"] |
| plugin | dev |
| updated | "2026-01-20T00:00:00.000Z" |
Frontend Testing Patterns
Overview
Testing patterns for frontend applications using Vitest and React Testing Library / Vue Test Utils.
Testing Philosophy
User-Centric Testing
Test behavior, not implementation. Query elements the way users would find them.
expect(wrapper.state('isOpen')).toBe(true);
expect(wrapper.find('.modal-class').exists()).toBe(true);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Modal Title')).toBeVisible();
Query Priority
Use queries in this order (most to least preferred):
getByRole - Accessible to everyone
getByLabelText - Form elements
getByPlaceholderText - Inputs
getByText - Non-interactive elements
getByDisplayValue - Form current values
getByAltText - Images
getByTestId - Last resort
Component Testing (React)
Basic Component Test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserCard } from './UserCard';
describe('UserCard', () => {
const user = { id: '1', name: 'John Doe', email: 'john@example.com' };
it('renders user information', () => {
render(<UserCard user={user} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('calls onSelect when clicked', async () => {
const onSelect = vi.fn();
const userEvt = userEvent.setup();
render(<UserCard user={user} onSelect={onSelect} />);
userEvt.(screen.());
(onSelect).(user);
});
});
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserList } from './UserList';
vi.mock('@/api', () => ({
getUsers: vi.fn(),
}));
describe('UserList', () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
queryClient.clear();
});
it('shows loading state', () => {
api.getUsers.mockImplementation(() => new Promise(() => {}));
render(, { wrapper });
(screen.()).();
});
(, () => {
api..([
{ : , : },
{ : , : },
]);
(, { wrapper });
( {
(screen.()).();
(screen.()).();
});
});
(, () => {
api..( ());
(, { wrapper });
( {
(screen.()).();
});
});
});
Testing Forms
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits form with valid data', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('shows validation errors', () => {
user = userEvent.();
();
user.(screen.(, { : }));
(screen.()).();
(screen.()).();
});
(, () => {
user = userEvent.();
(
);
user.(screen.(), );
user.(screen.(), );
user.(screen.(, { : }));
(screen.(, { : })).();
});
});
Component Testing (Vue)
Basic Component Test
import { mount } from '@vue/test-utils';
import UserCard from './UserCard.vue';
describe('UserCard', () => {
const user = { id: '1', name: 'John Doe', email: 'john@example.com' };
it('renders user information', () => {
const wrapper = mount(UserCard, {
props: { user },
});
expect(wrapper.text()).toContain('John Doe');
expect(wrapper.text()).toContain('john@example.com');
});
it('emits select event when clicked', async () => {
const wrapper = mount(UserCard, {
props: { user },
});
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('select')).toBeTruthy();
expect(wrapper.emitted()[]).([user]);
});
});
Testing with Pinia
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import UserList from './UserList.vue';
import { useUserStore } from '@/stores/userStore';
describe('UserList', () => {
it('renders users from store', () => {
const wrapper = mount(UserList, {
global: {
plugins: [
createTestingPinia({
initialState: {
user: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
},
}),
],
},
});
expect(wrapper.text()).toContain('John');
expect(wrapper.text()).toContain('Jane');
});
it('calls fetchUsers on mount', () => {
mount(UserList, {
: {
: [()],
},
});
store = ();
(store.).();
});
});
Hook Testing
import { renderHook, act, waitFor } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with custom value', () => {
const { result } = renderHook(() => useCounter({ initial: 10 }));
expect(result.current.count).toBe(10);
});
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe();
});
(, {
{ result } = ( ({ : , : }));
( {
result..();
result..();
});
(result..).();
});
});
Mocking
API Mocking
export const api = {
getUsers: vi.fn(),
createUser: vi.fn(),
updateUser: vi.fn(),
deleteUser: vi.fn(),
};
vi.mock('@/api');
beforeEach(() => {
vi.clearAllMocks();
});
it('fetches users', async () => {
api.getUsers.mockResolvedValue([{ id: '1', name: 'John' }]);
render(<UserList />);
await waitFor(() => {
expect(api.getUsers).toHaveBeenCalledTimes(1);
});
});
Module Mocking
vi.mock('@/utils/date', () => ({
formatDate: vi.fn(() => '2024-01-01'),
}));
vi.mock('@/config', async () => {
const actual = await vi.importActual('@/config');
return {
...actual,
API_URL: 'http://test-api.com',
};
});
Timer Mocking
describe('Debounced search', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('debounces search input', async () => {
const onSearch = vi.fn();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole('searchbox'), 'test');
expect(onSearch).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledWith('test');
});
});
Integration Testing
Testing Page Flow
describe('User Registration Flow', () => {
it('completes registration successfully', async () => {
const user = userEvent.setup();
render(<App />);
await user.click(screen.getByRole('link', { name: /register/i }));
await user.type(screen.getByLabelText(/name/i), 'John Doe');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.type(screen.getByLabelText(/password/i), 'SecurePass123!');
await user.click(screen.getByRole('button', { name: /register/i }));
await waitFor(() => {
expect(screen.getByText(/welcome, john/i)).();
});
});
});
Test Organization
File Structure
src/
├── components/
│ └── UserCard/
│ ├── UserCard.tsx
│ ├── UserCard.test.tsx
│ └── index.ts
├── hooks/
│ └── useAuth/
│ ├── useAuth.ts
│ └── useAuth.test.ts
└── __tests__/
└── integration/
└── auth.test.tsx
Test Setup
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Performance Testing
import { render } from '@testing-library/react';
import { performance } from 'perf_hooks';
describe('UserList Performance', () => {
it('renders 1000 items within 100ms', () => {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: String(i),
name: `User ${i}`,
}));
const start = performance.now();
render(<UserList items={items} />);
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
});
Accessibility Testing
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('has no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Frontend testing patterns for React and Vue applications