diff --git a/plugins/playwright/.claude-plugin/plugin.json b/plugins/playwright/.claude-plugin/plugin.json index e718f19..963fa6a 100644 --- a/plugins/playwright/.claude-plugin/plugin.json +++ b/plugins/playwright/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "playwright", - "version": "1.0.0", - "description": "Playwright testing utilities and test generation", + "version": "2.0.0", + "description": "Browser automation using Playwright for testing, screenshots, and web interaction. Provides best practices for browser management, timing, and cross-browser testing.", "author": "Val Redchenko" } diff --git a/plugins/playwright/README.md b/plugins/playwright/README.md index ed1d006..7c6fb6a 100644 --- a/plugins/playwright/README.md +++ b/plugins/playwright/README.md @@ -1,6 +1,6 @@ -# playwright +# Playwright Browser Automation Plugin -Plugin to set up and use Playwright browser automation for frontend web development. +Browser automation using Playwright for testing, screenshots, and web interaction. Use when you need to view web pages, take screenshots, interact with web apps, fill forms, click buttons, or verify UI behavior. ## Installation @@ -11,8 +11,351 @@ Plugin to set up and use Playwright browser automation for frontend web developm ## Commands -### `/playwright:init` -Initialize Playwright in the current project with configuration and example tests. +| Command | Description | +|---------|-------------| +| `/playwright:init` | Initialize Playwright in the current project | +| `/playwright:screenshot` | Take a screenshot of a web page | +| `/playwright:run` | Run a Playwright automation script | +| `/playwright:test` | Generate Playwright tests | -### `/playwright:generate` -Generate Playwright tests based on user requirements. +--- + +# Skill: Browser Automation Best Practices + +This plugin provides guidance for using Playwright effectively. Playwright is already an executable tool - the plugin's value is teaching best practices for browser management, timing, and cross-browser testing. + +## Setup + +Playwright should be installed as a dev dependency. Browser binaries are configured in `playwright.config.ts` and installed with: + +```bash +# npm +npx playwright install chromium firefox + +# bun +bunx playwright install chromium firefox +``` + +This installs both **Chromium** (default) and **Firefox** browsers. + +## Browser Selection + +Both Chromium and Firefox are available. Use this guidance: + +| Scenario | Browser | Rationale | +|----------|---------|-----------| +| Default/most testing | Chromium | Faster, better DevTools, primary development | +| Random variety (~20%) | Firefox | Catch cross-browser issues early | +| Both browsers | Both | CSS differences, before releases, debugging browser-specific bugs | + +### Selecting a Browser + +```typescript +import { chromium, firefox } from 'playwright'; + +// Default: Use Chromium +const browser = await chromium.launch(); + +// Random variety: ~80% Chromium, ~20% Firefox +function selectBrowser() { + return Math.random() < 0.8 ? chromium : firefox; +} +const browser = await selectBrowser().launch(); + +// Run same test in both browsers +for (const browserType of [chromium, firefox]) { + const browser = await browserType.launch(); + // ... run test ... + await browser.close(); +} +``` + +## CRITICAL: Browser Management Rules + +**Before launching any Playwright browser:** + +1. **Kill existing browser processes** - Never have multiple automated browsers running simultaneously +2. **Always use try/finally** - Ensure browser closes even on errors +3. **Set reasonable timeouts** - Detect hanging tests (default 30s is often too long) +4. **Run in foreground** - Do NOT run Playwright scripts in background mode + +```bash +# Kill any existing browser processes before starting +pkill -f chromium || true +pkill -f firefox || true +``` + +```typescript +// ALWAYS wrap in try/finally to ensure cleanup +const browser = await chromium.launch({ ... }); +try { + // ... test code ... +} finally { + await browser.close(); +} +``` + +## Action Timing Guidelines + +**Add delays between browser actions** to let the browser catch up: + +| Action Type | Recommended Delay | +|-------------|-------------------| +| After click | 150-300ms | +| After navigation | 500ms or `waitForLoadState` | +| After form fill | 100ms | +| After scroll/zoom | 200-300ms | +| Between rapid operations | 100-150ms | + +```typescript +// Example: Pacing interactions +await button.click(); +await page.waitForTimeout(200); // Let browser process the click + +await page.fill('input', 'value'); +await page.waitForTimeout(100); +``` + +## Headful Mode: Full Screen Setup + +When using headful mode (`headless: false`), maximize the browser window and match viewport to window size: + +```typescript +import { chromium } from 'playwright'; + +const browser = await chromium.launch({ + headless: false, + args: ['--start-maximized'] // Start maximized (Chromium) +}); + +// Create context without fixed viewport - uses full window size +const context = await browser.newContext({ + viewport: null // null = match browser window size +}); + +const page = await context.newPage(); +``` + +**Note:** Firefox uses `-start-maximized` (single dash) but `viewport: null` works for both. + +**Alternative: Explicit large viewport** +```typescript +const page = await browser.newPage({ + viewport: { width: 1920, height: 1080 } +}); +``` + +## Timeout and Hang Detection + +Set appropriate timeouts to detect hanging tests: + +```typescript +// Set default timeout for all operations (e.g., 15 seconds) +page.setDefaultTimeout(15000); + +// Or per-operation timeout +await page.click('button', { timeout: 5000 }); +await page.waitForSelector('.result', { timeout: 10000 }); + +// For locator operations +const box = await element.boundingBox({ timeout: 3000 }); +``` + +## Complete Script Template + +Use this template for all Playwright automation: + +```typescript +import { chromium, firefox, type Browser, type BrowserType } from 'playwright'; + +async function main() { + // Kill any existing browser processes + const { execSync } = await import('child_process'); + try { execSync('pkill -f chromium', { stdio: 'ignore' }); } catch {} + try { execSync('pkill -f firefox', { stdio: 'ignore' }); } catch {} + + // Select browser (80% Chromium, 20% Firefox for variety) + const browserType: BrowserType = Math.random() < 0.8 ? chromium : firefox; + console.log(`Using ${browserType.name()}`); + + let browser: Browser | null = null; + + try { + // Launch browser (headful with full screen) + browser = await browserType.launch({ + headless: false, + args: ['--start-maximized'] + }); + + const context = await browser.newContext({ viewport: null }); + const page = await context.newPage(); + + // Set reasonable default timeout + page.setDefaultTimeout(15000); + + await page.goto('http://localhost:5173'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(500); // Extra settling time + + // ... your test code here ... + // Remember to add delays between actions! + + // Take screenshot + await page.screenshot({ path: 'tmp/screenshot.png', fullPage: true }); + + } catch (error) { + console.error('Test failed:', error); + throw error; + } finally { + // ALWAYS close browser + if (browser) { + await browser.close(); + } + } +} + +main().catch(console.error); +``` + +## Quick Patterns + +### Take a Screenshot + +```typescript +import { chromium } from 'playwright'; + +let browser; +try { + browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto('http://localhost:5173'); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp/screenshot.png', fullPage: true }); +} finally { + if (browser) await browser.close(); +} +``` + +### Interactive Testing with Pacing + +```typescript +import { chromium } from 'playwright'; + +let browser; +try { + browser = await chromium.launch({ + headless: false, + args: ['--start-maximized'] + }); + const context = await browser.newContext({ viewport: null }); + const page = await context.newPage(); + page.setDefaultTimeout(10000); + + await page.goto('http://localhost:5173'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(500); + + // Click with pacing + await page.click('button:has-text("Submit")'); + await page.waitForTimeout(200); + + // Fill with pacing + await page.fill('input[name="email"]', 'test@example.com'); + await page.waitForTimeout(100); + + // Wait for result + await page.waitForSelector('.result'); + const text = await page.textContent('.result'); + +} finally { + if (browser) await browser.close(); +} +``` + +### Dual-Browser Testing + +Run the same test in both browsers for cross-browser validation: + +```typescript +import { chromium, firefox, type BrowserType } from 'playwright'; + +async function runTest(browserType: BrowserType) { + const browser = await browserType.launch(); + try { + const page = await browser.newPage(); + await page.goto('http://localhost:5173'); + // ... test code ... + console.log(`${browserType.name()}: PASS`); + } finally { + await browser.close(); + } +} + +// Run in both browsers +for (const browserType of [chromium, firefox]) { + await runTest(browserType); +} +``` + +### Check Element Visibility + +```typescript +const isVisible = await page.isVisible('.my-element'); +const count = await page.locator('.list-item').count(); +``` + +## Headless vs Headful Mode + +### When to Use Headful Mode (`headless: false`) + +Use headful mode for interactions that require full GPU/WebGL support: + +- **WebGL/Three.js/Canvas interactions** - Scroll-based zoom, raycasting, click detection on 3D meshes +- **Drag operations** - Complex drag-and-drop, canvas drawing +- **Scroll interactions** - Scroll-to-zoom, infinite scroll testing +- **Visual debugging** - When you need to see what's happening + +### When Headless Mode is Fine (`headless: true`) + +Headless works reliably for standard DOM interactions: + +- **Screenshots** - Static page captures +- **Form filling** - Input fields, dropdowns, checkboxes +- **Button clicks** - Standard DOM button elements +- **Text extraction** - Reading page content +- **Navigation** - Page loads, redirects +- **Basic assertions** - Element visibility, counts + +### User Override + +If the user explicitly requests headless or headful mode, follow their preference. + +## Common Commands + +```bash +# Kill existing browsers first +pkill -f chromium || true +pkill -f firefox || true + +# Install browsers +npx playwright install chromium firefox +# or with bun +bunx playwright install chromium firefox + +# Run a test script (FOREGROUND only, not background) +npx tsx scripts/screenshot.ts +# or with bun +bun run scripts/screenshot.ts +``` + +## Important Rules Summary + +1. **Never run multiple browsers** - Kill existing processes before launching new one +2. **Always use try/finally** - Browser must close even on error +3. **Run in foreground** - Do not use `run_in_background: true` for Playwright scripts +4. **Add delays between actions** - 100-300ms depending on operation type +5. **Set reasonable timeouts** - 10-15s default, shorter for simple operations +6. **Save screenshots to `tmp/`** - This directory should be gitignored +7. **Use `viewport: null` for headful** - Matches browser window size +8. **Use `--start-maximized`** - Full screen in headful mode +9. **Default to Chromium** - Use Firefox for variety (~20%) or cross-browser testing diff --git a/plugins/playwright/commands/generate.md b/plugins/playwright/commands/generate.md deleted file mode 100644 index 04baa70..0000000 --- a/plugins/playwright/commands/generate.md +++ /dev/null @@ -1,9 +0,0 @@ -Generate Playwright tests based on user requirements. The user will describe the test scenario and you will: - -1. Create a well-structured test file -2. Use best practices for selectors and assertions -3. Include proper setup and teardown -4. Add meaningful test descriptions -5. Handle common edge cases - -Ask the user to describe what they want to test. diff --git a/plugins/playwright/commands/init.md b/plugins/playwright/commands/init.md index 97388f4..1c7da23 100644 --- a/plugins/playwright/commands/init.md +++ b/plugins/playwright/commands/init.md @@ -1,9 +1,119 @@ -Initialize Playwright in the current project: +# Initialize Playwright -1. Install Playwright and its dependencies -2. Create playwright.config.ts with sensible defaults -3. Set up the tests/ directory structure -4. Install browsers if not present -5. Create an example test file +Set up Playwright browser automation in the current project. -Ask the user about their testing preferences (browsers, base URL, etc.). +## Steps + +### 1. Detect Project Type + +Check for package manager: +- If `bun.lockb` exists: use `bun` +- If `package-lock.json` exists: use `npm` +- Otherwise: ask the user which to use + +### 2. Install Playwright + +```bash +# npm +npm install -D playwright + +# bun +bun add -d playwright +``` + +### 3. Install Browsers + +Install Chromium and Firefox (the recommended browsers for testing): + +```bash +# npm +npx playwright install chromium firefox + +# bun +bunx playwright install chromium firefox +``` + +### 4. Create playwright.config.ts + +Create a configuration file with best practices: + +```typescript +import { defineConfig } from 'playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { browserName: 'chromium' }, + }, + { + name: 'firefox', + use: { browserName: 'firefox' }, + }, + ], +}); +``` + +### 5. Set Up Directory Structure + +Create necessary directories: + +```bash +mkdir -p tests +mkdir -p tmp # For screenshots (should be gitignored) +``` + +Add `tmp/` to `.gitignore` if not already present. + +### 6. Create Example Script + +Create `scripts/screenshot.ts` as a starting point: + +```typescript +import { chromium } from 'playwright'; + +async function main() { + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + await page.goto('http://localhost:5173'); + await page.waitForTimeout(500); + await page.screenshot({ path: 'tmp/screenshot.png', fullPage: true }); + console.log('Screenshot saved to tmp/screenshot.png'); + } finally { + await browser.close(); + } +} + +main().catch(console.error); +``` + +### 7. Add Package Scripts (Optional) + +Suggest adding to `package.json`: + +```json +{ + "scripts": { + "playwright:install": "playwright install chromium firefox", + "test:e2e": "playwright test" + } +} +``` + +## User Preferences to Ask + +Before starting, ask the user: +1. Base URL for testing (default: `http://localhost:5173`) +2. Whether to create example test files +3. Whether to install both Chromium and Firefox (recommended) or just Chromium diff --git a/plugins/playwright/commands/run.md b/plugins/playwright/commands/run.md new file mode 100644 index 0000000..c4c9259 --- /dev/null +++ b/plugins/playwright/commands/run.md @@ -0,0 +1,114 @@ +# Run Playwright Automation + +Execute a Playwright automation script with proper browser management. + +## Pre-Execution Checklist + +Before running any Playwright script: + +### 1. Kill Existing Browsers + +```bash +pkill -f chromium || true +pkill -f firefox || true +``` + +### 2. Verify Foreground Mode + +**CRITICAL**: Never run Playwright scripts in background mode. + +- Do NOT use `run_in_background: true` in Bash tool +- Browser automation must run in the foreground to work reliably + +### 3. Check Script Structure + +Ensure the script follows best practices: +- Uses try/finally for browser cleanup +- Has reasonable timeouts (10-15s default) +- Includes delays between actions (100-300ms) + +## Script Execution + +### With npm/npx + +```bash +# Kill browsers first +pkill -f chromium || true; pkill -f firefox || true + +# Run the script +npx tsx scripts/your-script.ts +``` + +### With Bun + +```bash +# Kill browsers first +pkill -f chromium || true; pkill -f firefox || true + +# Run the script +bun run scripts/your-script.ts +``` + +## Post-Execution + +### Check for Orphaned Processes + +If a script fails or hangs, clean up: + +```bash +pkill -f chromium || true +pkill -f firefox || true +``` + +### View Screenshots + +If the script saved screenshots to `tmp/`, read the image file to view it. + +## Troubleshooting + +### Script Hangs + +1. Kill the script (Ctrl+C) +2. Kill browser processes: `pkill -f chromium; pkill -f firefox` +3. Check for missing `await` statements +4. Reduce timeouts to catch hangs faster + +### Browser Won't Launch + +1. Ensure browsers are installed: `npx playwright install chromium firefox` +2. Check for existing processes: `pgrep -f chromium` +3. Kill all instances and retry + +### Element Not Found + +1. Add `waitForSelector` before interacting +2. Check if element is in iframe +3. Use `page.pause()` in headful mode to debug + +## Common Patterns + +### Wait for Network Idle + +```typescript +await page.goto('http://localhost:5173'); +await page.waitForLoadState('networkidle'); +``` + +### Wait for Specific Element + +```typescript +await page.waitForSelector('.my-element', { timeout: 10000 }); +``` + +### Retry on Failure + +```typescript +for (let i = 0; i < 3; i++) { + try { + await page.click('button'); + break; + } catch { + await page.waitForTimeout(500); + } +} +``` diff --git a/plugins/playwright/commands/screenshot.md b/plugins/playwright/commands/screenshot.md new file mode 100644 index 0000000..22e87ab --- /dev/null +++ b/plugins/playwright/commands/screenshot.md @@ -0,0 +1,76 @@ +# Take Screenshot + +Quickly capture a screenshot of a web page using Playwright. + +## Pre-Execution + +Before running, kill any existing browser processes: + +```bash +pkill -f chromium || true +pkill -f firefox || true +``` + +## Script Template + +Create a temporary script or use inline execution: + +```typescript +import { chromium } from 'playwright'; + +async function screenshot() { + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + + // Navigate to target URL + await page.goto('http://localhost:5173'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(500); // Let page settle + + // Take screenshot + await page.screenshot({ + path: 'tmp/screenshot.png', + fullPage: true + }); + + console.log('Screenshot saved to tmp/screenshot.png'); + } finally { + await browser.close(); + } +} + +screenshot().catch(console.error); +``` + +## Options + +Ask the user for: +- **URL**: The page to screenshot (default: `http://localhost:5173`) +- **Output path**: Where to save (default: `tmp/screenshot.png`) +- **Full page**: Capture entire scrollable page or just viewport + +## Screenshot Variants + +### Viewport Only +```typescript +await page.screenshot({ path: 'tmp/screenshot.png' }); +``` + +### Full Page (Scrollable) +```typescript +await page.screenshot({ path: 'tmp/screenshot.png', fullPage: true }); +``` + +### Specific Element +```typescript +const element = page.locator('.my-component'); +await element.screenshot({ path: 'tmp/component.png' }); +``` + +## Important + +- **Run in foreground** - Do NOT use `run_in_background: true` +- **Save to `tmp/`** - This directory should be gitignored +- **Always close browser** - Use try/finally pattern +- After taking the screenshot, read the image file to view it diff --git a/plugins/playwright/commands/test.md b/plugins/playwright/commands/test.md new file mode 100644 index 0000000..1308337 --- /dev/null +++ b/plugins/playwright/commands/test.md @@ -0,0 +1,173 @@ +# Generate Playwright Tests + +Generate Playwright tests based on user requirements. + +## Process + +1. Ask the user to describe what they want to test +2. Create a well-structured test file +3. Use best practices for selectors and assertions +4. Include proper setup and teardown +5. Add meaningful test descriptions + +## Test Template + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Feature Name', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + }); + + test('should do something specific', async ({ page }) => { + // Arrange + await page.waitForSelector('.target-element'); + + // Act + await page.click('button:has-text("Submit")'); + await page.waitForTimeout(200); // Let action complete + + // Assert + await expect(page.locator('.result')).toBeVisible(); + await expect(page.locator('.result')).toHaveText('Expected text'); + }); +}); +``` + +## Test Structure Guidelines + +### Use Descriptive Test Names + +```typescript +// Good +test('should display error message when email is invalid', ...); + +// Bad +test('test1', ...); +``` + +### Use Locator Best Practices + +Priority order for selectors: + +1. **Test IDs** (most reliable): `page.getByTestId('submit-button')` +2. **Role + name**: `page.getByRole('button', { name: 'Submit' })` +3. **Text content**: `page.getByText('Submit')` +4. **CSS selectors** (last resort): `page.locator('.submit-btn')` + +### Add Meaningful Assertions + +```typescript +// Check visibility +await expect(page.locator('.modal')).toBeVisible(); + +// Check text content +await expect(page.locator('.message')).toHaveText('Success!'); + +// Check attribute +await expect(page.locator('input')).toHaveAttribute('disabled', ''); + +// Check count +await expect(page.locator('.list-item')).toHaveCount(5); + +// Check URL +await expect(page).toHaveURL(/.*dashboard/); +``` + +## Cross-Browser Considerations + +Tests run against multiple browsers by default (Chromium, Firefox). Consider: + +- Avoid browser-specific CSS selectors +- Test with both browsers during development +- Use `test.skip` for known browser-specific issues: + +```typescript +test('WebGL feature', async ({ page, browserName }) => { + test.skip(browserName === 'firefox', 'WebGL not fully supported in Firefox'); + // ... test code +}); +``` + +## Handling Async Operations + +```typescript +// Wait for element to appear +await page.waitForSelector('.dynamic-content'); + +// Wait for network request +await page.waitForResponse(resp => + resp.url().includes('/api/data') && resp.status() === 200 +); + +// Wait for navigation +await Promise.all([ + page.waitForNavigation(), + page.click('a[href="/next-page"]') +]); +``` + +## Example Tests + +### Form Submission + +```typescript +test('should submit contact form', async ({ page }) => { + await page.fill('input[name="name"]', 'John Doe'); + await page.waitForTimeout(100); + + await page.fill('input[name="email"]', 'john@example.com'); + await page.waitForTimeout(100); + + await page.fill('textarea[name="message"]', 'Hello!'); + await page.waitForTimeout(100); + + await page.click('button[type="submit"]'); + await page.waitForTimeout(200); + + await expect(page.locator('.success-message')).toBeVisible(); +}); +``` + +### Navigation + +```typescript +test('should navigate to about page', async ({ page }) => { + await page.click('a:has-text("About")'); + await page.waitForTimeout(200); + + await expect(page).toHaveURL(/.*about/); + await expect(page.locator('h1')).toHaveText('About Us'); +}); +``` + +### Authentication + +```typescript +test('should login successfully', async ({ page }) => { + await page.fill('input[name="email"]', 'user@example.com'); + await page.fill('input[name="password"]', 'password123'); + await page.click('button:has-text("Login")'); + + await expect(page).toHaveURL(/.*dashboard/); + await expect(page.locator('.user-menu')).toBeVisible(); +}); +``` + +## Running Tests + +```bash +# Run all tests +npx playwright test + +# Run specific test file +npx playwright test tests/login.spec.ts + +# Run in headed mode (see browser) +npx playwright test --headed + +# Run specific browser +npx playwright test --project=chromium +```