component-testing
About
This skill helps developers test React components and pages using React Testing Library for unit tests and Playwright for E2E testing. It's ideal for verifying component logic, user interactions, accessibility, and complete user flows. The skill can write and run tests for both individual components and full application functionality.
Quick Install
Claude Code
Recommended/plugin add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/component-testingCopy and paste this command in Claude Code to install this skill
Documentation
You help test React components and pages for the QA Team Portal frontend using React Testing Library and Playwright.
When to Use This Skill
- Testing React components after creation
- Writing unit tests for component logic
- Testing user interactions (clicks, typing, form submission)
- E2E testing of complete user flows
- Accessibility testing
- Visual regression testing
Testing Approaches
1. Unit Tests with React Testing Library
Basic Component Test
// tests/unit/components/TeamMemberCard.test.tsx
import { render, screen } from '@testing-library/react'
import { TeamMemberCard } from '@/components/public/TeamIntro/TeamMemberCard'
const mockMember = {
id: '123',
name: 'John Doe',
role: 'QA Lead',
email: '[email protected]',
profilePhotoUrl: '/path/to/photo.jpg'
}
describe('TeamMemberCard', () => {
it('renders member name and role', () => {
render(<TeamMemberCard member={mockMember} />)
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('QA Lead')).toBeInTheDocument()
})
it('displays profile photo with alt text', () => {
render(<TeamMemberCard member={mockMember} />)
const img = screen.getByRole('img', { name: /john doe/i })
expect(img).toHaveAttribute('src', mockMember.profilePhotoUrl)
})
it('shows email link when provided', () => {
render(<TeamMemberCard member={mockMember} />)
const emailLink = screen.getByRole('link', { name: /email/i })
expect(emailLink).toHaveAttribute('href', 'mailto:[email protected]')
})
})
Testing User Interactions
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { UpdatesModal } from '@/components/public/Updates/UpdateModal'
describe('UpdatesModal', () => {
it('closes modal when close button clicked', async () => {
const onClose = vi.fn()
render(<UpdatesModal isOpen={true} onClose={onClose} update={mockUpdate} />)
const closeButton = screen.getByRole('button', { name: /close/i })
await userEvent.click(closeButton)
expect(onClose).toHaveBeenCalledTimes(1)
})
it('closes modal on escape key press', async () => {
const onClose = vi.fn()
render(<UpdatesModal isOpen={true} onClose={onClose} update={mockUpdate} />)
fireEvent.keyDown(document, { key: 'Escape' })
expect(onClose).toHaveBeenCalled()
})
})
Testing Forms
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from '@/components/admin/auth/LoginForm'
describe('LoginForm', () => {
it('submits form with valid data', async () => {
const onSubmit = vi.fn()
render(<LoginForm onSubmit={onSubmit} />)
await userEvent.type(
screen.getByLabelText(/email/i),
'[email protected]'
)
await userEvent.type(
screen.getByLabelText(/password/i),
'password123'
)
await userEvent.click(screen.getByRole('button', { name: /login/i }))
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
})
})
})
it('shows validation errors for invalid email', async () => {
render(<LoginForm onSubmit={vi.fn()} />)
await userEvent.type(
screen.getByLabelText(/email/i),
'invalid-email'
)
await userEvent.click(screen.getByRole('button', { name: /login/i }))
await waitFor(() => {
expect(screen.getByText(/invalid email/i)).toBeInTheDocument()
})
})
})
Testing API Integration
import { render, screen, waitFor } from '@testing-library/react'
import { TeamList } from '@/components/public/TeamIntro/TeamList'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const mockTeamMembers = [
{ id: '1', name: 'John Doe', role: 'QA Lead' },
{ id: '2', name: 'Jane Smith', role: 'QA Engineer' }
]
const server = setupServer(
rest.get('/api/v1/team-members', (req, res, ctx) => {
return res(ctx.json(mockTeamMembers))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('TeamList', () => {
it('displays loading state initially', () => {
render(<TeamList />)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('displays team members after loading', async () => {
render(<TeamList />)
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
})
})
it('displays error message on API failure', async () => {
server.use(
rest.get('/api/v1/team-members', (req, res, ctx) => {
return res(ctx.status(500))
})
)
render(<TeamList />)
await waitFor(() => {
expect(screen.getByText(/error loading/i)).toBeInTheDocument()
})
})
})
2. E2E Tests with Playwright
Setup Playwright
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:5173',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: !process.env.CI,
},
})
Basic E2E Test
// tests/e2e/landing-page.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Landing Page', () => {
test('displays all sections', async ({ page }) => {
await page.goto('/')
// Check all sections are visible
await expect(page.getByRole('heading', { name: /team introduction/i })).toBeVisible()
await expect(page.getByRole('heading', { name: /latest updates/i })).toBeVisible()
await expect(page.getByRole('heading', { name: /tools/i })).toBeVisible()
await expect(page.getByRole('heading', { name: /resources/i })).toBeVisible()
await expect(page.getByRole('heading', { name: /research/i })).toBeVisible()
})
test('navigation links scroll to sections', async ({ page }) => {
await page.goto('/')
// Click tools nav link
await page.getByRole('link', { name: /tools/i }).click()
// Check tools section is in view
const toolsSection = page.getByRole('heading', { name: /tools/i })
await expect(toolsSection).toBeInViewport()
})
})
Testing User Flows
// tests/e2e/admin-login.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Admin Login', () => {
test('admin can login and access dashboard', async ({ page }) => {
// Navigate to login page
await page.goto('/admin/login')
// Fill login form
await page.getByLabel(/email/i).fill('[email protected]')
await page.getByLabel(/password/i).fill('testpass123')
// Submit form
await page.getByRole('button', { name: /login/i }).click()
// Wait for redirect to dashboard
await expect(page).toHaveURL('/admin/dashboard')
// Check dashboard loads
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/admin/login')
await page.getByLabel(/email/i).fill('[email protected]')
await page.getByLabel(/password/i).fill('wrongpass')
await page.getByRole('button', { name: /login/i }).click()
// Check error message appears
await expect(page.getByText(/invalid credentials/i)).toBeVisible()
})
})
Testing CRUD Operations
// tests/e2e/team-management.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Team Management', () => {
test.beforeEach(async ({ page }) => {
// Login as admin
await page.goto('/admin/login')
await page.getByLabel(/email/i).fill('[email protected]')
await page.getByLabel(/password/i).fill('testpass123')
await page.getByRole('button', { name: /login/i }).click()
await page.waitForURL('/admin/dashboard')
// Navigate to team management
await page.getByRole('link', { name: /team members/i }).click()
})
test('can create new team member', async ({ page }) => {
await page.getByRole('button', { name: /add member/i }).click()
// Fill form
await page.getByLabel(/name/i).fill('New Member')
await page.getByLabel(/role/i).fill('QA Engineer')
await page.getByLabel(/email/i).fill('[email protected]')
// Upload photo
await page.getByLabel(/photo/i).setInputFiles('./tests/fixtures/profile.jpg')
// Submit
await page.getByRole('button', { name: /save/i }).click()
// Verify success message
await expect(page.getByText(/member created successfully/i)).toBeVisible()
// Verify appears in list
await expect(page.getByText('New Member')).toBeVisible()
})
test('can edit existing team member', async ({ page }) => {
// Click edit button for first member
await page.getByRole('row').first().getByRole('button', { name: /edit/i }).click()
// Update name
await page.getByLabel(/name/i).clear()
await page.getByLabel(/name/i).fill('Updated Name')
// Save
await page.getByRole('button', { name: /save/i }).click()
// Verify updated
await expect(page.getByText('Updated Name')).toBeVisible()
})
test('can delete team member', async ({ page }) => {
// Get initial count
const initialCount = await page.getByRole('row').count()
// Delete first member
await page.getByRole('row').first().getByRole('button', { name: /delete/i }).click()
// Confirm deletion
await page.getByRole('button', { name: /confirm/i }).click()
// Verify count decreased
const newCount = await page.getByRole('row').count()
expect(newCount).toBe(initialCount - 1)
})
})
3. Accessibility Testing
// tests/e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test.describe('Accessibility', () => {
test('landing page has no accessibility violations', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
test('admin dashboard has no accessibility violations', async ({ page }) => {
// Login first
await page.goto('/admin/login')
await page.getByLabel(/email/i).fill('[email protected]')
await page.getByLabel(/password/i).fill('testpass123')
await page.getByRole('button', { name: /login/i }).click()
await page.waitForURL('/admin/dashboard')
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
})
Running Tests
Vitest (Unit Tests)
cd frontend
# Run all unit tests
npm run test
# Run in watch mode
npm run test:watch
# Run with coverage
npm run test:coverage
# Run specific file
npm run test -- TeamMemberCard.test.tsx
# Run with UI
npm run test:ui
Playwright (E2E Tests)
cd frontend
# Install browsers (first time)
npx playwright install
# Run all E2E tests
npx playwright test
# Run in UI mode
npx playwright test --ui
# Run specific test file
npx playwright test tests/e2e/landing-page.spec.ts
# Run in headed mode (see browser)
npx playwright test --headed
# Run in debug mode
npx playwright test --debug
# Run on specific browser
npx playwright test --project=chromium
# Generate test code
npx playwright codegen http://localhost:5173
Test Configuration
Vitest Setup (vitest.config.ts)
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Test Setup File (tests/setup.ts)
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
// Cleanup after each test
afterEach(() => {
cleanup()
})
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
Test Checklist
For each component, verify:
- Rendering - Component renders without errors
- Props - Handles different prop combinations
- User interactions - Clicks, typing, form submission work
- Loading states - Shows loading indicators
- Error states - Shows error messages
- Empty states - Handles no data gracefully
- Accessibility - ARIA labels, keyboard navigation, screen readers
- Responsiveness - Works on mobile/tablet/desktop
- Edge cases - Null values, long text, special characters
Common Testing Patterns
Testing Hooks
import { renderHook, waitFor } from '@testing-library/react'
import { useTeamMembers } from '@/hooks/useTeamMembers'
test('useTeamMembers fetches data', async () => {
const { result } = renderHook(() => useTeamMembers())
expect(result.current.loading).toBe(true)
await waitFor(() => {
expect(result.current.loading).toBe(false)
expect(result.current.data).toHaveLength(2)
})
})
Testing Context
import { render, screen } from '@testing-library/react'
import { AuthProvider } from '@/contexts/AuthContext'
import { ProtectedComponent } from '@/components/ProtectedComponent'
test('shows content when authenticated', () => {
render(
<AuthProvider value={{ user: mockUser, isAuthenticated: true }}>
<ProtectedComponent />
</AuthProvider>
)
expect(screen.getByText(/protected content/i)).toBeInTheDocument()
})
Output Format
After testing, report:
- Tests Run: X passed, Y failed
- Coverage: X% of components/lines covered
- Failed Tests: List with error messages
- Accessibility Issues: WCAG violations found
- Performance: Slow-rendering components
- Recommendations: Suggested improvements
Best Practices
- Test user behavior, not implementation details
- Use semantic queries (getByRole, getByLabel, getByText)
- Avoid testing IDs or classes when possible
- Test accessibility (keyboard navigation, screen readers)
- Mock external dependencies (API calls, localStorage)
- Keep tests independent - no shared state
- Use descriptive test names - what you're testing and expected outcome
- Test error scenarios - not just happy path
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
creating-opencode-plugins
MetaThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
