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
typescript1// tests/unit/components/TeamMemberCard.test.tsx 2import { render, screen } from '@testing-library/react' 3import { TeamMemberCard } from '@/components/public/TeamIntro/TeamMemberCard' 4 5const mockMember = { 6 id: '123', 7 name: 'John Doe', 8 role: 'QA Lead', 9 email: 'john@example.com', 10 profilePhotoUrl: '/path/to/photo.jpg' 11} 12 13describe('TeamMemberCard', () => { 14 it('renders member name and role', () => { 15 render(<TeamMemberCard member={mockMember} />) 16 17 expect(screen.getByText('John Doe')).toBeInTheDocument() 18 expect(screen.getByText('QA Lead')).toBeInTheDocument() 19 }) 20 21 it('displays profile photo with alt text', () => { 22 render(<TeamMemberCard member={mockMember} />) 23 24 const img = screen.getByRole('img', { name: /john doe/i }) 25 expect(img).toHaveAttribute('src', mockMember.profilePhotoUrl) 26 }) 27 28 it('shows email link when provided', () => { 29 render(<TeamMemberCard member={mockMember} />) 30 31 const emailLink = screen.getByRole('link', { name: /email/i }) 32 expect(emailLink).toHaveAttribute('href', 'mailto:john@example.com') 33 }) 34})
Testing User Interactions
typescript1import { render, screen, fireEvent } from '@testing-library/react' 2import userEvent from '@testing-library/user-event' 3import { UpdatesModal } from '@/components/public/Updates/UpdateModal' 4 5describe('UpdatesModal', () => { 6 it('closes modal when close button clicked', async () => { 7 const onClose = vi.fn() 8 render(<UpdatesModal isOpen={true} onClose={onClose} update={mockUpdate} />) 9 10 const closeButton = screen.getByRole('button', { name: /close/i }) 11 await userEvent.click(closeButton) 12 13 expect(onClose).toHaveBeenCalledTimes(1) 14 }) 15 16 it('closes modal on escape key press', async () => { 17 const onClose = vi.fn() 18 render(<UpdatesModal isOpen={true} onClose={onClose} update={mockUpdate} />) 19 20 fireEvent.keyDown(document, { key: 'Escape' }) 21 22 expect(onClose).toHaveBeenCalled() 23 }) 24})
Testing Forms
typescript1import { render, screen, waitFor } from '@testing-library/react' 2import userEvent from '@testing-library/user-event' 3import { LoginForm } from '@/components/admin/auth/LoginForm' 4 5describe('LoginForm', () => { 6 it('submits form with valid data', async () => { 7 const onSubmit = vi.fn() 8 render(<LoginForm onSubmit={onSubmit} />) 9 10 await userEvent.type( 11 screen.getByLabelText(/email/i), 12 'admin@test.com' 13 ) 14 await userEvent.type( 15 screen.getByLabelText(/password/i), 16 'password123' 17 ) 18 19 await userEvent.click(screen.getByRole('button', { name: /login/i })) 20 21 await waitFor(() => { 22 expect(onSubmit).toHaveBeenCalledWith({ 23 email: 'admin@test.com', 24 password: 'password123' 25 }) 26 }) 27 }) 28 29 it('shows validation errors for invalid email', async () => { 30 render(<LoginForm onSubmit={vi.fn()} />) 31 32 await userEvent.type( 33 screen.getByLabelText(/email/i), 34 'invalid-email' 35 ) 36 await userEvent.click(screen.getByRole('button', { name: /login/i })) 37 38 await waitFor(() => { 39 expect(screen.getByText(/invalid email/i)).toBeInTheDocument() 40 }) 41 }) 42})
Testing API Integration
typescript1import { render, screen, waitFor } from '@testing-library/react' 2import { TeamList } from '@/components/public/TeamIntro/TeamList' 3import { rest } from 'msw' 4import { setupServer } from 'msw/node' 5 6const mockTeamMembers = [ 7 { id: '1', name: 'John Doe', role: 'QA Lead' }, 8 { id: '2', name: 'Jane Smith', role: 'QA Engineer' } 9] 10 11const server = setupServer( 12 rest.get('/api/v1/team-members', (req, res, ctx) => { 13 return res(ctx.json(mockTeamMembers)) 14 }) 15) 16 17beforeAll(() => server.listen()) 18afterEach(() => server.resetHandlers()) 19afterAll(() => server.close()) 20 21describe('TeamList', () => { 22 it('displays loading state initially', () => { 23 render(<TeamList />) 24 expect(screen.getByRole('status')).toBeInTheDocument() 25 }) 26 27 it('displays team members after loading', async () => { 28 render(<TeamList />) 29 30 await waitFor(() => { 31 expect(screen.getByText('John Doe')).toBeInTheDocument() 32 expect(screen.getByText('Jane Smith')).toBeInTheDocument() 33 }) 34 }) 35 36 it('displays error message on API failure', async () => { 37 server.use( 38 rest.get('/api/v1/team-members', (req, res, ctx) => { 39 return res(ctx.status(500)) 40 }) 41 ) 42 43 render(<TeamList />) 44 45 await waitFor(() => { 46 expect(screen.getByText(/error loading/i)).toBeInTheDocument() 47 }) 48 }) 49})
2. E2E Tests with Playwright
Setup Playwright
typescript1// playwright.config.ts 2import { defineConfig } from '@playwright/test' 3 4export default defineConfig({ 5 testDir: './tests/e2e', 6 use: { 7 baseURL: 'http://localhost:5173', 8 screenshot: 'only-on-failure', 9 video: 'retain-on-failure', 10 }, 11 webServer: { 12 command: 'npm run dev', 13 port: 5173, 14 reuseExistingServer: !process.env.CI, 15 }, 16})
Basic E2E Test
typescript1// tests/e2e/landing-page.spec.ts 2import { test, expect } from '@playwright/test' 3 4test.describe('Landing Page', () => { 5 test('displays all sections', async ({ page }) => { 6 await page.goto('/') 7 8 // Check all sections are visible 9 await expect(page.getByRole('heading', { name: /team introduction/i })).toBeVisible() 10 await expect(page.getByRole('heading', { name: /latest updates/i })).toBeVisible() 11 await expect(page.getByRole('heading', { name: /tools/i })).toBeVisible() 12 await expect(page.getByRole('heading', { name: /resources/i })).toBeVisible() 13 await expect(page.getByRole('heading', { name: /research/i })).toBeVisible() 14 }) 15 16 test('navigation links scroll to sections', async ({ page }) => { 17 await page.goto('/') 18 19 // Click tools nav link 20 await page.getByRole('link', { name: /tools/i }).click() 21 22 // Check tools section is in view 23 const toolsSection = page.getByRole('heading', { name: /tools/i }) 24 await expect(toolsSection).toBeInViewport() 25 }) 26})
Testing User Flows
typescript1// tests/e2e/admin-login.spec.ts 2import { test, expect } from '@playwright/test' 3 4test.describe('Admin Login', () => { 5 test('admin can login and access dashboard', async ({ page }) => { 6 // Navigate to login page 7 await page.goto('/admin/login') 8 9 // Fill login form 10 await page.getByLabel(/email/i).fill('admin@test.com') 11 await page.getByLabel(/password/i).fill('testpass123') 12 13 // Submit form 14 await page.getByRole('button', { name: /login/i }).click() 15 16 // Wait for redirect to dashboard 17 await expect(page).toHaveURL('/admin/dashboard') 18 19 // Check dashboard loads 20 await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible() 21 }) 22 23 test('shows error for invalid credentials', async ({ page }) => { 24 await page.goto('/admin/login') 25 26 await page.getByLabel(/email/i).fill('wrong@test.com') 27 await page.getByLabel(/password/i).fill('wrongpass') 28 29 await page.getByRole('button', { name: /login/i }).click() 30 31 // Check error message appears 32 await expect(page.getByText(/invalid credentials/i)).toBeVisible() 33 }) 34})
Testing CRUD Operations
typescript1// tests/e2e/team-management.spec.ts 2import { test, expect } from '@playwright/test' 3 4test.describe('Team Management', () => { 5 test.beforeEach(async ({ page }) => { 6 // Login as admin 7 await page.goto('/admin/login') 8 await page.getByLabel(/email/i).fill('admin@test.com') 9 await page.getByLabel(/password/i).fill('testpass123') 10 await page.getByRole('button', { name: /login/i }).click() 11 await page.waitForURL('/admin/dashboard') 12 13 // Navigate to team management 14 await page.getByRole('link', { name: /team members/i }).click() 15 }) 16 17 test('can create new team member', async ({ page }) => { 18 await page.getByRole('button', { name: /add member/i }).click() 19 20 // Fill form 21 await page.getByLabel(/name/i).fill('New Member') 22 await page.getByLabel(/role/i).fill('QA Engineer') 23 await page.getByLabel(/email/i).fill('new@test.com') 24 25 // Upload photo 26 await page.getByLabel(/photo/i).setInputFiles('./tests/fixtures/profile.jpg') 27 28 // Submit 29 await page.getByRole('button', { name: /save/i }).click() 30 31 // Verify success message 32 await expect(page.getByText(/member created successfully/i)).toBeVisible() 33 34 // Verify appears in list 35 await expect(page.getByText('New Member')).toBeVisible() 36 }) 37 38 test('can edit existing team member', async ({ page }) => { 39 // Click edit button for first member 40 await page.getByRole('row').first().getByRole('button', { name: /edit/i }).click() 41 42 // Update name 43 await page.getByLabel(/name/i).clear() 44 await page.getByLabel(/name/i).fill('Updated Name') 45 46 // Save 47 await page.getByRole('button', { name: /save/i }).click() 48 49 // Verify updated 50 await expect(page.getByText('Updated Name')).toBeVisible() 51 }) 52 53 test('can delete team member', async ({ page }) => { 54 // Get initial count 55 const initialCount = await page.getByRole('row').count() 56 57 // Delete first member 58 await page.getByRole('row').first().getByRole('button', { name: /delete/i }).click() 59 60 // Confirm deletion 61 await page.getByRole('button', { name: /confirm/i }).click() 62 63 // Verify count decreased 64 const newCount = await page.getByRole('row').count() 65 expect(newCount).toBe(initialCount - 1) 66 }) 67})
3. Accessibility Testing
typescript1// tests/e2e/accessibility.spec.ts 2import { test, expect } from '@playwright/test' 3import AxeBuilder from '@axe-core/playwright' 4 5test.describe('Accessibility', () => { 6 test('landing page has no accessibility violations', async ({ page }) => { 7 await page.goto('/') 8 9 const accessibilityScanResults = await new AxeBuilder({ page }).analyze() 10 11 expect(accessibilityScanResults.violations).toEqual([]) 12 }) 13 14 test('admin dashboard has no accessibility violations', async ({ page }) => { 15 // Login first 16 await page.goto('/admin/login') 17 await page.getByLabel(/email/i).fill('admin@test.com') 18 await page.getByLabel(/password/i).fill('testpass123') 19 await page.getByRole('button', { name: /login/i }).click() 20 21 await page.waitForURL('/admin/dashboard') 22 23 const accessibilityScanResults = await new AxeBuilder({ page }).analyze() 24 25 expect(accessibilityScanResults.violations).toEqual([]) 26 }) 27})
Running Tests
Vitest (Unit Tests)
bash1cd frontend 2 3# Run all unit tests 4npm run test 5 6# Run in watch mode 7npm run test:watch 8 9# Run with coverage 10npm run test:coverage 11 12# Run specific file 13npm run test -- TeamMemberCard.test.tsx 14 15# Run with UI 16npm run test:ui
Playwright (E2E Tests)
bash1cd frontend 2 3# Install browsers (first time) 4npx playwright install 5 6# Run all E2E tests 7npx playwright test 8 9# Run in UI mode 10npx playwright test --ui 11 12# Run specific test file 13npx playwright test tests/e2e/landing-page.spec.ts 14 15# Run in headed mode (see browser) 16npx playwright test --headed 17 18# Run in debug mode 19npx playwright test --debug 20 21# Run on specific browser 22npx playwright test --project=chromium 23 24# Generate test code 25npx playwright codegen http://localhost:5173
Test Configuration
Vitest Setup (vitest.config.ts)
typescript1import { defineConfig } from 'vitest/config' 2import react from '@vitejs/plugin-react' 3import path from 'path' 4 5export default defineConfig({ 6 plugins: [react()], 7 test: { 8 globals: true, 9 environment: 'jsdom', 10 setupFiles: './tests/setup.ts', 11 }, 12 resolve: { 13 alias: { 14 '@': path.resolve(__dirname, './src'), 15 }, 16 }, 17})
Test Setup File (tests/setup.ts)
typescript1import '@testing-library/jest-dom' 2import { cleanup } from '@testing-library/react' 3import { afterEach, vi } from 'vitest' 4 5// Cleanup after each test 6afterEach(() => { 7 cleanup() 8}) 9 10// Mock window.matchMedia 11Object.defineProperty(window, 'matchMedia', { 12 writable: true, 13 value: vi.fn().mockImplementation(query => ({ 14 matches: false, 15 media: query, 16 onchange: null, 17 addListener: vi.fn(), 18 removeListener: vi.fn(), 19 addEventListener: vi.fn(), 20 removeEventListener: vi.fn(), 21 dispatchEvent: vi.fn(), 22 })), 23})
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
typescript1import { renderHook, waitFor } from '@testing-library/react' 2import { useTeamMembers } from '@/hooks/useTeamMembers' 3 4test('useTeamMembers fetches data', async () => { 5 const { result } = renderHook(() => useTeamMembers()) 6 7 expect(result.current.loading).toBe(true) 8 9 await waitFor(() => { 10 expect(result.current.loading).toBe(false) 11 expect(result.current.data).toHaveLength(2) 12 }) 13})
Testing Context
typescript1import { render, screen } from '@testing-library/react' 2import { AuthProvider } from '@/contexts/AuthContext' 3import { ProtectedComponent } from '@/components/ProtectedComponent' 4 5test('shows content when authenticated', () => { 6 render( 7 <AuthProvider value={{ user: mockUser, isAuthenticated: true }}> 8 <ProtectedComponent /> 9 </AuthProvider> 10 ) 11 12 expect(screen.getByText(/protected content/i)).toBeInTheDocument() 13})
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