testing-qa
Expert guide for testing Next.js applications with Playwright, Jest, and React Testing Library. Use when writing tests, debugging test failures, or setting up test infrastructure.
What this skill does
# Testing & QA Skill
## Overview
This skill helps you write comprehensive tests for Next.js applications using Playwright for E2E tests, Jest for unit tests, and React Testing Library for component tests.
## Testing Philosophy
### Testing Pyramid
1. **E2E Tests (10%)**: Critical user journeys
2. **Integration Tests (30%)**: Component interactions
3. **Unit Tests (60%)**: Individual functions and utilities
### What to Test
- **DO**: Test behavior, not implementation
- **DO**: Test user interactions and outcomes
- **DO**: Test error states and edge cases
- **DO**: Test accessibility
- **DON'T**: Test internal implementation details
- **DON'T**: Test third-party libraries
- **DON'T**: Over-test simple presentational components
## Playwright E2E Tests
### Setup
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
```
### Basic E2E Test
```typescript
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Authentication', () => {
test('should sign up new user', async ({ page }) => {
await page.goto('/signup')
// Fill form
await page.fill('input[name="email"]', '[email protected]')
await page.fill('input[name="password"]', 'password123')
await page.fill('input[name="confirmPassword"]', 'password123')
// Submit
await page.click('button[type="submit"]')
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard')
// Verify welcome message
await expect(page.getByText('Welcome')).toBeVisible()
})
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="email"]', '[email protected]')
await page.fill('input[name="password"]', 'wrongpassword')
await page.click('button[type="submit"]')
// Verify error message
await expect(page.getByText('Invalid credentials')).toBeVisible()
})
})
```
### Advanced Playwright Patterns
```typescript
// Page Object Model
// e2e/pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.page.fill('input[name="email"]', email)
await this.page.fill('input[name="password"]', password)
await this.page.click('button[type="submit"]')
}
async getErrorMessage() {
return await this.page.locator('[role="alert"]').textContent()
}
}
// Usage
test('login with page object', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('[email protected]', 'password')
await expect(page).toHaveURL('/dashboard')
})
// Fixtures for authenticated state
// e2e/fixtures.ts
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// Login
await page.goto('/login')
await page.fill('input[name="email"]', '[email protected]')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('/dashboard')
await use(page)
},
})
// Usage
test('dashboard test', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard')
// Test authenticated functionality
})
```
### Common Playwright Patterns
```typescript
// Wait for network
await page.waitForResponse(resp => resp.url().includes('/api/items'))
// Test file upload
await page.setInputFiles('input[type="file"]', 'path/to/file.jpg')
// Test download
const downloadPromise = page.waitForEvent('download')
await page.click('button:has-text("Download")')
const download = await downloadPromise
await download.saveAs('/path/to/save')
// Mock API responses
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ items: [] }),
})
})
// Screenshot for debugging
await page.screenshot({ path: 'debug.png', fullPage: true })
// Test responsive design
await page.setViewportSize({ width: 375, height: 667 }) // iPhone size
// Test accessibility
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
```
## Component Testing
### Setup React Testing Library
```typescript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
}
// jest.setup.js
import '@testing-library/jest-dom'
```
### Basic Component Test
```typescript
// components/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './button'
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>)
expect(screen.getByText('Click me')).toBeDisabled()
})
it('applies variant styles', () => {
render(<Button variant="destructive">Delete</Button>)
const button = screen.getByText('Delete')
expect(button).toHaveClass('bg-red-600')
})
})
```
### Testing Async Components
```typescript
// components/user-profile.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { UserProfile } from './user-profile'
// Mock fetch
global.fetch = jest.fn()
describe('UserProfile', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('displays loading state initially', () => {
(fetch as jest.Mock).mockImplementation(() =>
new Promise(() => {}) // Never resolves
)
render(<UserProfile userId="123" />)
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
it('displays user data when loaded', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ name: 'John Doe', email: '[email protected]' }),
})
render(<UserProfile userId="123" />)
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('[email protected]')).toBeInTheDocument()
})
})
it('displays error message when fetch fails', async () => {
(fetch as jest.Mock).mockRejectedValueOnce(new Error('Failed to fetch'))
render(<UserProfile userId="123" />)
await waitFor(() => {
expect(screen.getByText('Error loading user')).toBeInTheDocument()
})
})
})
```
### Testing Forms
```typescript
// components/contact-form.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ContactForm } from './contact-form'
describe('ContactForm', () => {
it('validates required fields', async () => {
render(<ContactForm />)
const submitButton = screen.getByText('Submit')
fireEvent.click(submitButton)
await waitFor(() => {
expect(screen.getByText('Email is required')).toBeInTheDocument()
expect(screen.getByRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.