--- name: browser-testing description: Browser automation with Playwright for E2E tests and Claude's Chrome MCP for rapid development verification. Use when writing E2E tests or verifying UI during development. --- # Browser Testing Skill ## Two Approaches ### 1. Claude Chrome MCP (Development Verification) Quick, interactive testing during development to verify UI works before writing permanent tests. ### 2. Playwright (Permanent E2E Tests) Automated, repeatable tests that run in CI/CD. --- ## Claude Chrome MCP (Quick Verification) Use Chrome MCP tools for rapid UI verification during development. ### Basic Workflow ``` 1. Navigate to page 2. Take screenshot to verify state 3. Find and interact with elements 4. Verify expected outcome 5. Record GIF for complex flows ``` ### Navigation ```typescript // Navigate to URL mcp__claude-in-chrome__navigate({ url: "http://localhost:5173", tabId: 123 }) // Go back/forward mcp__claude-in-chrome__navigate({ url: "back", tabId: 123 }) ``` ### Finding Elements ```typescript // Find by natural language mcp__claude-in-chrome__find({ query: "login button", tabId: 123 }) mcp__claude-in-chrome__find({ query: "email input field", tabId: 123 }) mcp__claude-in-chrome__find({ query: "product card containing organic", tabId: 123 }) ``` ### Reading Page State ```typescript // Get accessibility tree (best for understanding structure) mcp__claude-in-chrome__read_page({ tabId: 123, filter: "interactive" }) // Get text content (for articles/text-heavy pages) mcp__claude-in-chrome__get_page_text({ tabId: 123 }) ``` ### Interactions ```typescript // Click element by reference mcp__claude-in-chrome__computer({ action: "left_click", ref: "ref_42", tabId: 123 }) // Type text mcp__claude-in-chrome__computer({ action: "type", text: "user@example.com", tabId: 123 }) // Fill form input mcp__claude-in-chrome__form_input({ ref: "ref_15", value: "test@example.com", tabId: 123 }) // Press keys mcp__claude-in-chrome__computer({ action: "key", text: "Enter", tabId: 123 }) ``` ### Screenshots and GIFs ```typescript // Take screenshot mcp__claude-in-chrome__computer({ action: "screenshot", tabId: 123 }) // Record GIF for complex interaction mcp__claude-in-chrome__gif_creator({ action: "start_recording", tabId: 123 }) // ... perform actions ... mcp__claude-in-chrome__gif_creator({ action: "export", download: true, filename: "login-flow.gif", tabId: 123 }) ``` ### Verification Pattern ``` 1. Navigate to the page 2. Screenshot to see current state 3. Find the element you want to interact with 4. Interact with it 5. Screenshot to verify the result 6. If complex flow, record a GIF ``` --- ## Playwright (Permanent E2E Tests) ### Project 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'], ['junit', { outputFile: 'results.xml' }], ], use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, { name: 'mobile', use: { ...devices['iPhone 13'] }, }, ], webServer: { command: 'npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, }, }); ``` ### Page Object Pattern ```typescript // e2e/pages/login.page.ts import { Page, Locator, expect } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Sign in' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } async expectError(message: string) { await expect(this.errorMessage).toContainText(message); } async expectLoggedIn() { await expect(this.page).toHaveURL('/dashboard'); } } ``` ### Test Patterns ```typescript // e2e/auth/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; test.describe('Login', () => { let loginPage: LoginPage; test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page); await loginPage.goto(); }); test('should show validation errors for empty form', async () => { await loginPage.submitButton.click(); await expect(loginPage.page.getByText('Email is required')).toBeVisible(); await expect(loginPage.page.getByText('Password is required')).toBeVisible(); }); test('should show error for invalid credentials', async () => { await loginPage.login('wrong@example.com', 'wrongpassword'); await loginPage.expectError('Invalid credentials'); }); test('should redirect to dashboard on successful login', async () => { await loginPage.login('user@example.com', 'correctpassword'); await loginPage.expectLoggedIn(); await expect(loginPage.page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); }); test('should persist session after page reload', async ({ page }) => { await loginPage.login('user@example.com', 'correctpassword'); await loginPage.expectLoggedIn(); await page.reload(); await expect(page).toHaveURL('/dashboard'); }); }); ``` ### API Mocking ```typescript // e2e/dashboard/projects.spec.ts import { test, expect } from '@playwright/test'; test.describe('Projects Dashboard', () => { test('should display projects from API', async ({ page }) => { // Mock API response await page.route('**/api/projects', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: '1', name: 'Project A', status: 'active' }, { id: '2', name: 'Project B', status: 'completed' }, ]), }); }); await page.goto('/dashboard'); await expect(page.getByText('Project A')).toBeVisible(); await expect(page.getByText('Project B')).toBeVisible(); }); test('should show error state on API failure', async ({ page }) => { await page.route('**/api/projects', async (route) => { await route.fulfill({ status: 500 }); }); await page.goto('/dashboard'); await expect(page.getByRole('alert')).toContainText('Failed to load projects'); }); }); ``` ### Visual Regression Testing ```typescript // e2e/visual/components.spec.ts import { test, expect } from '@playwright/test'; test.describe('Visual Regression', () => { test('login page matches snapshot', async ({ page }) => { await page.goto('/login'); await expect(page).toHaveScreenshot('login-page.png'); }); test('dashboard matches snapshot', async ({ page }) => { // Setup authenticated state await page.goto('/dashboard'); await expect(page).toHaveScreenshot('dashboard.png', { mask: [page.locator('.timestamp')], // Mask dynamic content }); }); }); ``` ### Accessibility Testing ```typescript // e2e/a11y/accessibility.spec.ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test.describe('Accessibility', () => { test('login page should have no accessibility violations', async ({ page }) => { await page.goto('/login'); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); test('dashboard should have no accessibility violations', async ({ page }) => { await page.goto('/dashboard'); const results = await new AxeBuilder({ page }) .exclude('.third-party-widget') // Exclude third-party content .analyze(); expect(results.violations).toEqual([]); }); }); ``` ## Commands ```bash # Playwright npx playwright test # Run all tests npx playwright test --project=chromium # Specific browser npx playwright test login.spec.ts # Specific file npx playwright test --ui # Interactive UI mode npx playwright test --debug # Debug mode npx playwright show-report # View HTML report npx playwright codegen # Generate tests by recording # Update snapshots npx playwright test --update-snapshots ``` ## Best Practices 1. **Use role-based locators** (accessible to all) ```typescript page.getByRole('button', { name: 'Submit' }) page.getByLabel('Email') ``` 2. **Wait for specific conditions** (not arbitrary timeouts) ```typescript await expect(page.getByText('Success')).toBeVisible(); ``` 3. **Isolate tests** (each test sets up its own state) ```typescript test.beforeEach(async ({ page }) => { await page.goto('/login'); }); ``` 4. **Use Page Objects** (DRY, maintainable) ```typescript const loginPage = new LoginPage(page); await loginPage.login(email, password); ``` 5. **Mock external APIs** (fast, reliable) ```typescript await page.route('**/api/**', handler); ```