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
typescript
1// 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
typescript
1import { 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})
typescript
1import { 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
typescript
1import { 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
typescript
1// 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
typescript
1// 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
typescript
1// 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
typescript
1// 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
typescript
1// 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)
bash
1cd 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)
bash
1cd 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)
typescript
1import { 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)
typescript
1import '@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:
Common Testing Patterns
Testing Hooks
typescript
1import { 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
typescript
1import { 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})
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