.agents/skills/playwright-best-practices/debugging/error-testing.md
test('error boundary catches component error', async ({page}) => {
// Trigger error via mock
await page.route('**/api/user', (route) => {
route.fulfill({
json: null, // Will cause component to throw
})
})
await page.goto('/profile')
// Error boundary should render fallback
await expect(page.getByText('Something went wrong')).toBeVisible()
await expect(page.getByRole('button', {name: 'Try Again'})).toBeVisible()
})
test('recover from error state', async ({page}) => {
let requestCount = 0
await page.route('**/api/data', (route) => {
requestCount++
if (requestCount === 1) {
return route.fulfill({status: 500})
}
return route.fulfill({
json: {data: 'success'},
})
})
await page.goto('/dashboard')
// Error state
await expect(page.getByText('Failed to load')).toBeVisible()
// Retry
await page.getByRole('button', {name: 'Retry'}).click()
// Success state
await expect(page.getByText('success')).toBeVisible()
})
test('handles runtime error gracefully', async ({page}) => {
const errors: string[] = []
page.on('pageerror', (error) => {
errors.push(error.message)
})
await page.goto('/buggy-page')
// App should still be functional despite error
await expect(page.getByRole('navigation')).toBeVisible()
// Error was logged
expect(errors.length).toBeGreaterThan(0)
})
test.describe('API error handling', () => {
const errorCodes = [400, 401, 403, 404, 500, 502, 503]
for (const status of errorCodes) {
test(`handles ${status} error`, async ({page}) => {
await page.route('**/api/data', (route) =>
route.fulfill({
status,
json: {error: `Error ${status}`},
}),
)
await page.goto('/dashboard')
// Appropriate error message shown
await expect(page.getByRole('alert')).toBeVisible()
})
}
})
test('handles request timeout', async ({page}) => {
await page.route('**/api/slow', async (route) => {
// Never respond - simulates timeout
await new Promise(() => {})
})
await page.goto('/slow-page')
// Should show timeout message (app should have its own timeout)
await expect(page.getByText('Request timed out')).toBeVisible({
timeout: 15000,
})
})
test('handles connection failure', async ({page}) => {
await page.route('**/api/data', (route) => {
route.abort('connectionfailed')
})
await page.goto('/dashboard')
await expect(page.getByText('Connection failed')).toBeVisible()
await expect(page.getByRole('button', {name: 'Retry'})).toBeVisible()
})
test('handles failure during request', async ({page}) => {
let requestStarted = false
await page.route('**/api/upload', async (route) => {
requestStarted = true
// Abort after small delay (mid-request)
await new Promise((resolve) => setTimeout(resolve, 500))
route.abort('failed')
})
await page.goto('/upload')
await page.getByLabel('File').setInputFiles('./fixtures/large-file.pdf')
await page.getByRole('button', {name: 'Upload'}).click()
// Should show failure, not hang
await expect(page.getByText('Upload failed')).toBeVisible()
expect(requestStarted).toBe(true)
})
This section covers unexpected network failures and error recovery. For offline-first apps (PWAs) with service workers, caching, and background sync, see service-workers.md.
test('handles going offline', async ({page, context}) => {
await page.goto('/dashboard')
await expect(page.getByTestId('data')).toBeVisible()
// Go offline unexpectedly
await context.setOffline(true)
// Try to refresh data
await page.getByRole('button', {name: 'Refresh'}).click()
// Should show offline indicator
await expect(page.getByText("You're offline")).toBeVisible()
// Go back online
await context.setOffline(false)
// Should recover
await page.getByRole('button', {name: 'Refresh'}).click()
await expect(page.getByText("You're offline")).toBeHidden()
})
test('recovers gracefully when connection returns', async ({page, context}) => {
await page.goto('/dashboard')
// Simulate connection drop
await context.setOffline(true)
// App should show degraded state
await expect(page.getByRole('alert')).toContainText(/offline|connection/i)
// Connection restored
await context.setOffline(false)
// Retry should work
await page.getByRole('button', {name: 'Retry'}).click()
await expect(page.getByTestId('data')).toBeVisible()
})
test('shows skeleton during load', async ({page}) => {
// Add delay to API response
await page.route('**/api/posts', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
route.fulfill({
json: [{id: 1, title: 'Post 1'}],
})
})
await page.goto('/posts')
// Skeleton should appear immediately
await expect(page.getByTestId('skeleton')).toBeVisible()
// Then content replaces skeleton
await expect(page.getByText('Post 1')).toBeVisible()
await expect(page.getByTestId('skeleton')).toBeHidden()
})
test('shows loading state for actions', async ({page}) => {
await page.route('**/api/save', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 500))
route.fulfill({json: {success: true}})
})
await page.goto('/editor')
await page.getByLabel('Content').fill('New content')
const saveButton = page.getByRole('button', {name: 'Save'})
await saveButton.click()
// Button should show loading state
await expect(saveButton).toBeDisabled()
await expect(page.getByTestId('spinner')).toBeVisible()
// Then success state
await expect(saveButton).toBeEnabled()
await expect(page.getByText('Saved')).toBeVisible()
})
test('shows empty state when no data', async ({page}) => {
await page.route('**/api/items', (route) => route.fulfill({json: []}))
await page.goto('/items')
await expect(page.getByText('No items yet')).toBeVisible()
await expect(page.getByRole('button', {name: 'Create First Item'})).toBeVisible()
})
test('validates required fields', async ({page}) => {
await page.goto('/signup')
// Submit empty form
await page.getByRole('button', {name: 'Sign Up'}).click()
// Should show validation errors
await expect(page.getByText('Email is required')).toBeVisible()
await expect(page.getByText('Password is required')).toBeVisible()
// Form should not submit
await expect(page).toHaveURL('/signup')
})
test('validates email format', async ({page}) => {
await page.goto('/signup')
await page.getByLabel('Email').fill('invalid-email')
await page.getByLabel('Email').blur()
await expect(page.getByText('Invalid email address')).toBeVisible()
// Fix the error
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Email').blur()
await expect(page.getByText('Invalid email address')).toBeHidden()
})
test('handles server validation errors', async ({page}) => {
await page.route('**/api/register', (route) =>
route.fulfill({
status: 422,
json: {
errors: {
email: 'Email already exists',
username: 'Username is taken',
},
},
}),
)
await page.goto('/signup')
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Username').fill('takenuser')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', {name: 'Sign Up'}).click()
// Server errors should display
await expect(page.getByText('Email already exists')).toBeVisible()
await expect(page.getByText('Username is taken')).toBeVisible()
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Only testing happy path | Misses error handling bugs | Test all error scenarios |
| No network failure tests | App crashes on poor connection | Test offline/slow/failed requests |
| Skipping loading states | Janky UX not caught | Assert loading UI appears |
| Ignoring validation | Form bugs slip through | Test both client and server validation |