Claude
Skills
Sign in
Back

testing-frontend

Included with Lifetime
$97 forever

Use when writing component tests, testing user interactions, mocking APIs, or setting up Vitest/React Testing Library/Vue Test Utils for frontend applications.

Design

What this skill does


# 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.

```tsx
// BAD: Testing implementation
expect(wrapper.state('isOpen')).toBe(true);
expect(wrapper.find('.modal-class').exists()).toBe(true);

// GOOD: Testing behavior
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Modal Title')).toBeVisible();
```

### Query Priority

Use queries in this order (most to least preferred):

1. `getByRole` - Accessible to everyone
2. `getByLabelText` - Form elements
3. `getByPlaceholderText` - Inputs
4. `getByText` - Non-interactive elements
5. `getByDisplayValue` - Form current values
6. `getByAltText` - Images
7. `getByTestId` - Last resort

## Component Testing (React)

### Basic Component Test

```tsx
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: '[email protected]' };

  it('renders user information', () => {
    render(<UserCard user={user} />);

    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
  });

  it('calls onSelect when clicked', async () => {
    const onSelect = vi.fn();
    const userEvt = userEvent.setup();

    render(<UserCard user={user} onSelect={onSelect} />);

    await userEvt.click(screen.getByRole('button'));

    expect(onSelect).toHaveBeenCalledWith(user);
  });
});
```

### Testing Async Components

```tsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserList } from './UserList';

// Mock API
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(<UserList />, { wrapper });

    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  it('shows users when loaded', async () => {
    api.getUsers.mockResolvedValue([
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' },
    ]);

    render(<UserList />, { wrapper });

    await waitFor(() => {
      expect(screen.getByText('John')).toBeInTheDocument();
      expect(screen.getByText('Jane')).toBeInTheDocument();
    });
  });

  it('shows error message on failure', async () => {
    api.getUsers.mockRejectedValue(new Error('Network error'));

    render(<UserList />, { wrapper });

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});
```

### Testing Forms

```tsx
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), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123',
    });
  });

  it('shows validation errors', async () => {
    const user = userEvent.setup();

    render(<LoginForm onSubmit={vi.fn()} />);

    // Submit empty form
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });

  it('disables submit button while submitting', async () => {
    const user = userEvent.setup();

    render(
      <LoginForm
        onSubmit={() => new Promise((resolve) => setTimeout(resolve, 100))}
      />
    );

    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(screen.getByRole('button', { name: /submitting/i })).toBeDisabled();
  });
});
```

## Component Testing (Vue)

### Basic Component Test

```ts
import { mount } from '@vue/test-utils';
import UserCard from './UserCard.vue';

describe('UserCard', () => {
  const user = { id: '1', name: 'John Doe', email: '[email protected]' };

  it('renders user information', () => {
    const wrapper = mount(UserCard, {
      props: { user },
    });

    expect(wrapper.text()).toContain('John Doe');
    expect(wrapper.text()).toContain('[email protected]');
  });

  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('select')[0]).toEqual([user]);
  });
});
```

### Testing with Pinia

```ts
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, {
      global: {
        plugins: [createTestingPinia()],
      },
    });

    const store = useUserStore();
    expect(store.fetchUsers).toHaveBeenCalled();
  });
});
```

## Hook Testing

```tsx
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(1);
  });

  it('respects max limit', () => {
    const { result } = renderHook(() => useCounter({ initial: 9, max: 10 }));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    expect(result.current.count).toBe(10);
  });
});
```

## Mocking

### API Mocking

```tsx
// __mocks__/api.ts
export const api = {
  getUsers: vi.fn(),
  createUser: vi.fn(),
  updateUser: vi.fn(),
  deleteUser: vi.fn(),
};

// In test
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

```tsx
// Mock entire modu

Related in Design