From claude-skills
Write tests for TruckBays features using Vitest, React Testing Library, and MSW v2. Use when writing tests, adding test coverage, or setting up testing infrastructure.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-skills:frontend-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write tests that give confidence your app works — not tests that verify implementation details.
Write tests that give confidence your app works — not tests that verify implementation details.
"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds
Implementation details are things users of your code will not typically use, see, or know about. Testing them leads to:
// ❌ DON'T: Test hook return values directly
const { result } = renderHook(() => useNavbar())
expect(result.current.isOpen).toBe(false) // user never sees "isOpen"
// ✅ DO: Test what the user sees and does
render(<Navbar />)
await userEvent.click(screen.getByLabelText(/menu/i))
expect(screen.getByRole('navigation')).toBeVisible()
Tests should only assert on things these two users care about.
renderHook IS appropriateuseAuth) — the hook IS the public developer APIFor hooks that power a component (useNavbar, useBackHeader, useLanguageSelector), test through the component instead.
| Tool | Purpose |
|---|---|
| Vitest | Test runner and assertions |
| React Testing Library | Component and hook rendering |
| @testing-library/user-event | Simulating user interactions |
| MSW v2 | API mocking at the network level |
| jsdom | Browser environment simulation |
__tests__/ directories.
components/navbar.tsx → components/navbar.test.tsxproviders/auth-provider.tsx → providers/auth-provider.test.tsxdomain/{feature}.service.ts → domain/{feature}.service.test.tsapi/{feature}.api.ts → api/{feature}.api.test.ts.test.ts or .test.tsx suffixAlways prefer queries that match how users and assistive technology interact with your app. Follow this priority order:
Queries elements exposed in the accessibility tree. Best for testing accessibility and should be your default choice.
// Buttons
screen.getByRole('button', { name: /submit/i })
screen.getByRole('button', { name: /cancel/i })
// Form fields
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('textbox', { name: /password/i })
screen.getByRole('combobox', { name: /country/i })
// Navigation
screen.getByRole('navigation')
screen.getByRole('link', { name: /home/i })
// Headings
screen.getByRole('heading', { name: /dashboard/i, level: 1 })
For form fields associated with labels. Excellent for accessibility.
screen.getByLabelText(/email address/i)
screen.getByLabelText(/password/i)
When a label is not available (less ideal than label text).
screen.getByPlaceholderText(/search\.\.\./i)
For non-interactive elements (paragraphs, divs, spans) or finding by text content.
screen.getByText(/welcome back/i)
screen.getByText(/no results found/i)
Only use when semantic queries don't work or don't make sense (e.g., dynamic text, non-semantic wrappers).
// Avoid this when possible
screen.getByTestId('user-avatar')
Why this order matters:
getByRole and getByLabelText test accessibilityThis is where most tests should live. Render the component, interact as a user would, assert on visible output.
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
describe('Navbar', () => {
it('opens the menu when hamburger is clicked', async () => {
render(<Navbar />)
// Use getByRole for buttons with accessible name
await userEvent.click(screen.getByRole('button', { name: /menu/i }))
expect(screen.getByRole('navigation')).toBeVisible()
})
it('hides auth-required items when unauthenticated', async () => {
render(<Navbar />) // default: unauthenticated
await userEvent.click(screen.getByRole('button', { name: /menu/i }))
expect(screen.queryByText(/messages/i)).not.toBeInTheDocument()
})
it('navigates to home when logo is clicked', async () => {
const mockPush = vi.fn()
vi.mock('next/router', () => ({ useRouter: () => ({ push: mockPush }) }))
render(<Navbar />)
await userEvent.click(screen.getByRole('link', { name: /home/i }))
expect(mockPush).toHaveBeenCalledWith('/')
})
})
The hook IS the public API for developers. Test it directly.
import { renderHook, waitFor } from '@testing-library/react'
import { AuthProvider, useAuth } from './auth-provider'
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>
describe('useAuth', () => {
it('returns authenticated when valid user in localStorage', async () => {
localStorage.setItem('user', JSON.stringify(validUser))
const { result } = renderHook(() => useAuth(), { wrapper })
await waitFor(() => expect(result.current.isLoading).toBe(false))
expect(result.current.isAuthenticated).toBe(true)
})
})
No mocking needed. Test inputs → outputs.
import { canEdit, getStatusLabel } from '../{feature}.service'
describe('{feature}.service', () => {
it('returns true for active items', () => {
expect(canEdit({ status: 'active' })).toBe(true)
})
})
Use MSW to mock HTTP at the network level.
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
const server = setupServer()
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('{feature}Api', () => {
it('maps response correctly', async () => {
server.use(
http.get('*/{feature}s', () => HttpResponse.json({ results: [{ id: 1 }] }))
)
const result = await {feature}Api.getAll({})
expect(result.results).toHaveLength(1)
})
})
When testing async behavior, use waitFor for:
import { waitFor } from '@testing-library/react'
it('displays data after loading', async () => {
render(<UserProfile userId="123" />)
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
})
// Assert final state
expect(screen.getByText(/john doe/i)).toBeInTheDocument()
})
For simpler cases, use findBy* queries (combines getBy + waitFor):
it('displays data after loading', async () => {
render(<UserProfile userId="123" />)
// Automatically waits for element to appear
expect(await screen.findByText(/john doe/i)).toBeInTheDocument()
})
React Testing Library methods handle act() automatically. If you see act() warnings:
What it means:
How to fix:
// ❌ BAD: Missing await
userEvent.click(button)
expect(screen.getByText(/success/i)).toBeInTheDocument()
// ✅ GOOD: Await user interaction
await userEvent.click(button)
expect(screen.getByText(/success/i)).toBeInTheDocument()
// ✅ GOOD: Wait for async updates
await userEvent.click(button)
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument()
})
General rule:
await userEvent methodswaitFor or findBy* for async assertionsWhen a domain type (e.g., Reservation) is used across many test files, create a shared test factory instead of duplicating makeReservation in every test file. This avoids a cascade of updates when adding a new required field to the type.
features/{feature}/
testing/
factories.ts # Shared test factories for this feature
// features/reservation-details/testing/factories.ts
import type { Reservation, ReservationInvoice } from '../domain/reservation.types'
export const createReservation = (overrides: Partial<Reservation> = {}): Reservation => ({
id: 1,
status: 'checked in',
type: 'daily',
// ... all required fields with sensible defaults ...
...overrides,
})
export const createInvoice = (overrides: Partial<ReservationInvoice> = {}): ReservationInvoice => ({
id: 1,
status: 'paid',
total: 5000,
createdAt: '2026-01-01T00:00:00Z',
collectionMethod: 'charge_automatically',
...overrides,
})
// domain/reservation-state.test.ts
import { createReservation, createInvoice } from '../testing/factories'
it('returns CHARGED_FAILED when own payment failed', () => {
const r = createReservation({ payment: { status: 'payment failed' } })
expect(resolveState(r)).toBe('CHARGED_FAILED')
})
it('detects unpaid invoice', () => {
const r = createReservation({
invoice: createInvoice({ status: 'open' }),
})
expect(resolveState(r)).toBe('CHARGED_FAILED')
})
If a type is only tested in 1-2 files, a local makeX helper is fine.
For API layer tests, create a separate factory for the raw API response shape:
export const createApiResponse = (overrides: Record<string, unknown> = {}) => ({
id: 1,
status: 'checked in',
// ... raw API shape ...
...overrides,
})
[ ] vitest.config.ts configured with jsdom, path aliases, setup file
[ ] vitest.setup.ts imports @testing-library/jest-dom
[ ] Component tests: render + userEvent + screen assertions
[ ] Provider tests: renderHook for public API hooks only
[ ] Domain tests: pure function input/output
[ ] API tests: MSW + mapper validation
[ ] NO hook-level tests for hooks that power components
[ ] package.json scripts: test, test:watch, test:coverage
If context was cleaned mid-pipeline, restore state before proceeding:
After completing this skill, use the AskUserQuestion tool to present the next step options. Include a summary of what was completed in the question text.
Options to present:
Do NOT present numbered text options and ask the user to "type a number." Always use the AskUserQuestion tool for skill transitions.
After completing this skill's work, report the context usage percentage so the user can decide whether to clean context:
"{Skill output summary}. Context usage: {X}%"
Do NOT recommend cleaning context — just show the percentage. The user will decide.
npx claudepluginhub longjohnsilver1504/claude-skillsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.