component-testing

Test React components with React Testing Library and Playwright. Use when building UI components or verifying frontend functionality.

Installs: 0
Used in: 1 repos
Updated: 2d ago
$npx ai-builder add skill PrasadTelasula/component-testing

Installs to .claude/skills/component-testing/

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
// 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: 'john@example.com',
  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:john@example.com')
  })
})
```

#### Testing User Interactions

```typescript
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

```typescript
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),
      'admin@test.com'
    )
    await userEvent.type(
      screen.getByLabelText(/password/i),
      'password123'
    )

    await userEvent.click(screen.getByRole('button', { name: /login/i }))

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'admin@test.com',
        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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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('admin@test.com')
    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('wrong@test.com')
    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

```typescript
// 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('admin@test.com')
    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('new@test.com')

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

```typescript
// 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('admin@test.com')
    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)

```bash
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)

```bash
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)

```typescript
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)

```typescript
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

```typescript
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

```typescript
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:

1. **Tests Run**: X passed, Y failed
2. **Coverage**: X% of components/lines covered
3. **Failed Tests**: List with error messages
4. **Accessibility Issues**: WCAG violations found
5. **Performance**: Slow-rendering components
6. **Recommendations**: Suggested improvements

## Best Practices

1. **Test user behavior**, not implementation details
2. **Use semantic queries** (getByRole, getByLabel, getByText)
3. **Avoid testing IDs or classes** when possible
4. **Test accessibility** (keyboard navigation, screen readers)
5. **Mock external dependencies** (API calls, localStorage)
6. **Keep tests independent** - no shared state
7. **Use descriptive test names** - what you're testing and expected outcome
8. **Test error scenarios** - not just happy path

Quick Install

$npx ai-builder add skill PrasadTelasula/component-testing

Details

Type
skill
Slug
PrasadTelasula/component-testing
Created
6d ago