8.6 KiB
Puppeteer Testing Patterns
This document provides reusable patterns for UI testing with puppeteer MCP.
Overview
puppeteer MCP provides browser automation tools accessible via MCP (Model Context Protocol):
puppeteer_launch()- Launch browserpuppeteer_navigate()- Navigate to URLpuppeteer_click()- Click elementpuppeteer_type()- Type text into inputpuppeteer_get_text()- Extract text from elementpuppeteer_screenshot()- Capture screenshotpuppeteer_evaluate()- Execute JavaScript in page contextpuppeteer_wait_for_selector()- Wait for element to appear
Common Patterns
Pattern 1: Form Submission
Use case: Test login, registration, profile update forms
Steps:
- Navigate to page
- Fill form fields
- Submit form
- Verify redirect or success message
Example (Login):
// Phase 1: Navigate
await page.goto('http://localhost:3000/login');
// Phase 2: Fill form
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'test123');
// Phase 3: Submit
await page.click('button[type="submit"]');
// Phase 4: Verify
await page.waitForURL('**/dashboard');
const heading = await page.textContent('h1');
expect(heading).toBe('Dashboard');
Selectors:
- Prefer
[name="fieldname"]for inputs - Prefer
[type="submit"]for buttons - Prefer
[data-testid="component"]if available
Pattern 2: Navigation and Assertion
Use case: Verify page renders correctly, content displayed
Steps:
- Navigate to page
- Wait for key element
- Assert content
Example (User List):
// Navigate
await page.goto('http://localhost:3000/users');
// Wait for content
await page.waitForSelector('table[data-testid="user-table"]');
// Assert
const heading = await page.textContent('h1');
expect(heading).toBe('User List');
const rowCount = await page.evaluate(() => {
return document.querySelectorAll('table tbody tr').length;
});
expect(rowCount).toBeGreaterThan(0);
Pattern 3: Element Interaction
Use case: Click buttons, toggle switches, open modals
Steps:
- Click trigger element
- Wait for result element
- Verify state change
Example (Add User Modal):
// Click button
await page.click('button[data-testid="add-user"]');
// Wait for modal
await page.waitForSelector('form[data-testid="user-form"]');
// Verify modal visible
const modalVisible = await page.isVisible('form[data-testid="user-form"]');
expect(modalVisible).toBe(true);
Pattern 4: Error Message Verification
Use case: Test validation errors, API error messages
Steps:
- Perform action that triggers error
- Wait for error message element
- Verify error text
Example (Invalid Login):
// Fill with invalid credentials
await page.fill('[name="email"]', 'wrong@example.com');
await page.fill('[name="password"]', 'wrong');
// Submit
await page.click('button[type="submit"]');
// Wait for error
await page.waitForSelector('[data-testid="error-message"]');
// Verify error text
const errorText = await page.textContent('[data-testid="error-message"]');
expect(errorText).toBe('Invalid email or password');
Important: Verify user-friendly message (no stack traces, technical jargon)
Pattern 5: Screenshot on Failure
Use case: Capture visual evidence when test fails
Steps:
- Wrap test in try-catch
- On error: take screenshot before throwing
Example:
try {
await page.click('button.non-existent');
} catch (error) {
// Capture screenshot
await page.screenshot({
path: `screenshots/failure_${Date.now()}.png`,
fullPage: true
});
// Re-throw error
throw new Error(`Test failed: ${error.message}. Screenshot saved.`);
}
Pattern 6: Async Wait Patterns
Use case: Wait for elements, network requests, animations
Wait for selector:
await page.waitForSelector('[data-testid="user-profile"]', {
timeout: 5000
});
Wait for URL change:
await page.waitForURL('**/dashboard', {
timeout: 5000
});
Wait for network idle:
await page.waitForLoadState('networkidle');
Wait for custom condition:
await page.waitForFunction(() => {
return document.querySelectorAll('table tbody tr').length > 0;
}, { timeout: 5000 });
Pattern 7: Extract Dynamic Data
Use case: Verify API data rendered correctly
Example (User Profile):
// Navigate to profile
await page.goto('http://localhost:3000/users/123');
// Wait for profile data
await page.waitForSelector('[data-testid="user-name"]');
// Extract data
const name = await page.textContent('[data-testid="user-name"]');
const email = await page.textContent('[data-testid="user-email"]');
const role = await page.textContent('[data-testid="user-role"]');
// Verify
expect(name).toBe('John Doe');
expect(email).toBe('john@example.com');
expect(role).toBe('Admin');
Pattern 8: Multi-Step Flow
Use case: Complex user journeys (e.g., checkout flow)
Example (User Registration → Email Verification → Login):
// Step 1: Register
await page.goto('http://localhost:3000/register');
await page.fill('[name="email"]', 'newuser@example.com');
await page.fill('[name="password"]', 'password123');
await page.fill('[name="name"]', 'New User');
await page.click('button[type="submit"]');
// Step 2: Verify redirect to verification page
await page.waitForURL('**/verify-email');
const message = await page.textContent('[data-testid="message"]');
expect(message).toContain('Check your email');
// Step 3: (Simulate email verification - in real test, would check email)
// For manual testing, verify verification email sent via integration test
// Step 4: Login with new account
await page.goto('http://localhost:3000/login');
await page.fill('[name="email"]', 'newuser@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Step 5: Verify logged in
await page.waitForURL('**/dashboard');
const welcomeText = await page.textContent('[data-testid="welcome"]');
expect(welcomeText).toContain('Welcome, New User');
Selector Best Practices
Priority order:
- data-testid attributes (best - stable, semantic)
- name attributes (good for forms)
- type attributes (good for buttons)
- role attributes (accessibility, semantic)
- class/id (avoid - brittle, implementation detail)
Examples:
// ✅ GOOD: Semantic, stable
await page.click('[data-testid="submit-button"]');
await page.fill('[name="email"]', 'test@example.com');
await page.click('button[type="submit"]');
await page.click('[role="button"]');
// ❌ BAD: Brittle, coupled to implementation
await page.click('.btn-primary.submit-btn');
await page.fill('#emailInput', 'test@example.com');
Error Handling
Pattern: Graceful Degradation
If puppeteer unavailable (e.g., server doesn't support browser automation):
try {
await page.goto('http://localhost:3000');
} catch (error) {
if (error.message.includes('net::ERR_CONNECTION_REFUSED')) {
return {
verdict: "ERROR",
message: "Application not running. Start server first."
};
}
throw error;
}
Pattern: Timeout Handling
try {
await page.waitForSelector('[data-testid="profile"]', {
timeout: 5000
});
} catch (error) {
if (error.message.includes('Timeout')) {
return {
verdict: "FAIL",
details: "Profile component did not render within 5 seconds"
};
}
throw error;
}
Integration with ln-343-manual-tester
When to use puppeteer (Phase 1 detection):
- Story type: UI
- Story description contains: "UI", "frontend", "page", "component"
- Story labels: "ui", "frontend", "react", "vue"
Workflow integration:
- Phase 3 (Test AC): Use puppeteer patterns for each AC
- Phase 4 (Edge Cases): Test invalid UI interactions
- Phase 5 (Error Handling): Verify error messages displayed
- Phase 7 (Document): Include puppeteer commands in temp script
Temp script format (UI tests):
#!/bin/bash
# Temporary manual testing script for Story US001 (UI)
# Created: 2025-11-13
# Note: UI tests require manual verification
# Puppeteer commands below for reference
echo "AC1: User can login"
echo "Steps:"
echo "1. Navigate to http://localhost:3000/login"
echo "2. Fill email: user@example.com"
echo "3. Fill password: test123"
echo "4. Click Submit"
echo "5. Verify redirect to /dashboard"
echo ""
echo "Run automated UI test:"
echo "npm run test:ui -- --grep 'User login'"
Version: 1.0.0 Last Updated: 2025-11-13