← All Skills
AI Skill

playwright-skill

Last updated: 2026-05-17

Complete browser automation with Playwright. Auto-detects dev servers, writes clean test scripts to /tmp. Test pages, fill forms, take screenshots, check respon

Quick Install
npx skills add playwright-skill
IMPORTANT - Path Resolution: This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). Before executing any commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands below. Replace $SKILL_DIR with the actual discovered path.

Common installation paths:

  • Plugin system: ~/.claude/plugins/marketplaces/playwright-skill/skills/playwright-skill
  • Manual global: ~/.claude/skills/playwright-skill
  • Project-specific: /.claude/skills/playwright-skill

Playwright Browser Automation

General-purpose browser automation skill. I'll write custom Playwright code for any automation task you request and execute it via the universal executor.

🚨 AUTO-TRIGGER: Image Upload Errors

Trigger Condition

When you see ANY of these errors:

"At least one of the image dimensions exceed max allowed size: 8000 pixels"
"API Error: 400"
"image.source.base64.data: At least one of the image dimensions exceed max allowed size"

Automatic Response (DO NOT ASK USER)

Step 1: Compress the image
cd $SKILL_DIR && node -e "require('./lib/helpers').compressImage().then(r => console.log('✅ Compressed:', r.outputPath))"
Step 2: Display the compressed image Use the Read tool on /tmp/compressed_screenshot.png Step 3: Inform user Say something like:
"图片太大了,我帮你压缩了 (666KB → 186KB)"

Critical Rules

NEVER ask: "需要我压缩吗?" or "Should I compress?" ❌ NEVER wait for user confirmation ✅ ALWAYS execute compression immediately ✅ ALWAYS display the compressed image after compression

Why This Matters

Users expect instant fixes, not questions. When an upload fails due to size:

  1. The problem is obvious (image too big)
  2. The solution is obvious (compress it)
  3. There's no decision to make (just do it)
Auto-fix is the expected behavior.

🎨 World-Class UX Evaluation Mode

USE THIS MODE WHEN:
  • User says "check the UX", "is it professional?", "does it look good?", "丑得一逼"
  • After UI changes, need to validate quality
  • Comparing against industry standards

UX Evaluation Framework

Reference Standards (Always compare against):
  • ✅ Vercel Docs: https://vercel.com/docs
  • ✅ Stripe Docs: https://stripe.com/docs
  • ✅ Linear: https://linear.app
  • ✅ Mintlify: https://mintlify.com
  • ✅ Tailwind UI: https://tailwindui.com
Critical UX Dimensions:
  1. Typography & Readability
  2. - [ ] Heading hierarchy clear (H1 > H2 > H3 distinct sizes) - [ ] Body text: 16-18px, line-height 1.5-1.7 - [ ] Color contrast ≥ 4.5:1 for body text (WCAG AA) - [ ] Max line length 60-80 characters - [ ] Consistent font weights (not too many variations)
  3. Spacing & Layout
  4. - [ ] Consistent spacing scale (8px, 16px, 24px, 32px...) - [ ] Adequate white space (not cramped) - [ ] Proper padding: buttons (12px 24px), sections (48px+ vertical) - [ ] Visual breathing room between sections - [ ] Grid alignment (elements don't look randomly placed)
  5. Color & Visual Hierarchy
  6. - [ ] Primary color used sparingly (CTAs, active states) - [ ] Clear visual hierarchy (important → less important) - [ ] Muted backgrounds, not competing with content - [ ] Shadows subtle and consistent - [ ] No harsh black (#000), use dark gray (#1a1a1a, #111)
  7. Interactive Elements
  8. - [ ] Buttons have clear hover/active states - [ ] Links distinguishable (color, underline, or weight) - [ ] Focus states visible for keyboard nav - [ ] Loading states for async actions - [ ] Disabled states clearly different
  9. Modern Design Patterns
  10. - [ ] Rounded corners (4px-12px for cards, buttons) - [ ] Subtle gradients (not 2010s style) - [ ] Shadows: sm (0-2px), md (4-12px), lg (8-24px) - [ ] Icons: 16px (inline), 20-24px (buttons), 32px+ (features) - [ ] Glassmorphism/backdrop-blur if applicable
  11. Responsive Design
  12. - [ ] Mobile (375px): single column, burger menu - [ ] Tablet (768px): adapted layout - [ ] Desktop (1440px+): full layout, proper max-width - [ ] No horizontal scroll - [ ] Touch targets ≥ 44px on mobile
  13. Performance Perception
  14. - [ ] Smooth transitions (200-300ms) - [ ] No layout shift during load - [ ] Skeleton loaders or progressive disclosure - [ ] Optimistic UI updates

UX Evaluation Script Template

// /tmp/playwright-ux-evaluation.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3000/docs'; // Auto-detected

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

console.log('\n🎨 World-Class UX Evaluation\n'); console.log(📍 Target: ${TARGET_URL}\n);

// Desktop view await page.setViewportSize({ width: 1440, height: 900 }); await page.goto(TARGET_URL, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000);

// ===== 1. Typography Analysis ===== console.log('📝 Typography & Readability:');

const typography = await page.evaluate(() => { const h1 = document.querySelector('h1'); const h2 = document.querySelector('h2'); const p = document.querySelector('p');

const h1Styles = h1 ? window.getComputedStyle(h1) : null; const h2Styles = h2 ? window.getComputedStyle(h2) : null; const pStyles = p ? window.getComputedStyle(p) : null;

return { h1: h1Styles ? { size: h1Styles.fontSize, weight: h1Styles.fontWeight, lineHeight: h1Styles.lineHeight, color: h1Styles.color } : null, h2: h2Styles ? { size: h2Styles.fontSize, weight: h2Styles.fontWeight } : null, body: pStyles ? { size: pStyles.fontSize, lineHeight: pStyles.lineHeight, color: pStyles.color, maxWidth: pStyles.maxWidth } : null }; });

console.log( H1: ${typography.h1?.size} / weight ${typography.h1?.weight}); console.log( H2: ${typography.h2?.size} / weight ${typography.h2?.weight}); console.log( Body: ${typography.body?.size} / line-height ${typography.body?.lineHeight});

// Check readability const bodySize = parseInt(typography.body?.size); const typographyScore = bodySize >= 16 && bodySize <= 18 ? '✅' : '❌'; console.log( ${typographyScore} Body text size (target: 16-18px)\n);

// ===== 2. Spacing Analysis ===== console.log('📏 Spacing & Layout:');

const spacing = await page.evaluate(() => { const main = document.querySelector('main') || document.body; const styles = window.getComputedStyle(main);

return { padding: styles.padding, maxWidth: styles.maxWidth, margin: styles.margin }; });

console.log( Content padding: ${spacing.padding}); console.log( Max width: ${spacing.maxWidth});

const hasMaxWidth = spacing.maxWidth !== 'none' && spacing.maxWidth !== '100%'; console.log( ${hasMaxWidth ? '✅' : '❌'} Proper max-width (prevents super-wide text)\n);

// ===== 3. Color Contrast Check ===== console.log('🎨 Color & Contrast:');

const contrast = await page.evaluate(() => { const getRGB = (color) => { const temp = document.createElement('div'); temp.style.color = color; document.body.appendChild(temp); const computed = window.getComputedStyle(temp).color; document.body.removeChild(temp);

const match = computed.match(/rgb\((\d+),\s(\d+),\s(\d+)\)/); return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : null; };

const getLuminance = (r, g, b) => { const [rs, gs, bs] = [r, g, b].map(c => { c = c / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); return 0.2126 rs + 0.7152 gs + 0.0722 bs; };

const getContrast = (rgb1, rgb2) => { const lum1 = getLuminance(...rgb1); const lum2 = getLuminance(...rgb2); const brightest = Math.max(lum1, lum2); const darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); };

const p = document.querySelector('p'); if (!p) return null;

const textColor = window.getComputedStyle(p).color; const bgColor = window.getComputedStyle(p).backgroundColor;

const textRGB = getRGB(textColor); const bgRGB = getRGB(bgColor) || [255, 255, 255]; // Default to white

return { textColor, bgColor, contrast: textRGB ? getContrast(textRGB, bgRGB).toFixed(2) : null }; });

if (contrast) { const contrastValue = parseFloat(contrast.contrast); const contrastScore = contrastValue >= 4.5 ? '✅' : '❌'; console.log( Text: ${contrast.textColor} on ${contrast.bgColor}); console.log( ${contrastScore} Contrast ratio: ${contrast.contrast}:1 (WCAG AA requires 4.5:1)\n); }

// ===== 4. Interactive Elements ===== console.log('🔘 Interactive Elements:');

const buttons = await page.locator('button, a[class="button"], a[class="btn"]').all(); console.log( Found ${buttons.length} buttons/CTAs);

if (buttons.length > 0) { const buttonStyles = await buttons[0].evaluate(el => { const styles = window.getComputedStyle(el); return { padding: styles.padding, borderRadius: styles.borderRadius, fontSize: styles.fontSize }; }); console.log( Button padding: ${buttonStyles.padding}); console.log( Border radius: ${buttonStyles.borderRadius});

const hasRoundedCorners = parseInt(buttonStyles.borderRadius) >= 4; console.log( ${hasRoundedCorners ? '✅' : '❌'} Modern rounded corners\n); }

// ===== 5. Responsive Check ===== console.log('📱 Responsive Design:');

const viewports = [ { name: 'Mobile', width: 375, height: 812 }, { name: 'Desktop', width: 1440, height: 900 } ];

for (const viewport of viewports) { await page.setViewportSize(viewport); await page.waitForTimeout(1000);

const hasHorizontalScroll = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; });

const scrollStatus = hasHorizontalScroll ? '❌ Horizontal scroll detected!' : '✅'; console.log( ${scrollStatus} ${viewport.name} (${viewport.width}px));

await page.screenshot({ path: /tmp/ux-${viewport.name.toLowerCase()}.png, fullPage: false }); }

console.log('\n📊 Overall Assessment:\n'); console.log('Compare screenshots against:'); console.log(' - Vercel Docs: Professional, clean, excellent spacing'); console.log(' - Stripe Docs: Perfect typography hierarchy'); console.log(' - Linear: Modern gradients, smooth interactions'); console.log(' - Mintlify: Outstanding navigation UX\n');

console.log('🎯 Key Improvements (if needed):'); console.log(' 1. Ensure 16-18px body text with 1.5-1.7 line-height'); console.log(' 2. Add proper spacing scale (16px, 24px, 32px, 48px)'); console.log(' 3. Use subtle shadows instead of borders'); console.log(' 4. Add hover states to all interactive elements'); console.log(' 5. Ensure 4.5:1+ color contrast for readability\n');

await browser.close(); })();

How to Use UX Evaluation Mode

Step 1: Run evaluation
cd $SKILL_DIR && node run.js /tmp/playwright-ux-evaluation.js
Step 2: Analyze output
  • Automated checks: typography, spacing, contrast, buttons
  • Manual review: Compare screenshots to Vercel/Stripe/Linear
  • Identify specific issues with ✅/❌ markers
Step 3: Provide feedback Not just "looks good" - give specific, actionable feedback:
UX Evaluation Results:

✅ GOOD:

  • Button padding and rounded corners modern (12px 24px, 8px radius)
  • Responsive design working (no horizontal scroll)
❌ NEEDS IMPROVEMENT:
  • Body text too small (14px, should be 16-18px)
  • Contrast ratio 3.2:1 (needs 4.5:1 minimum)
  • H1/H2 hierarchy weak (H1: 32px, H2: 28px - not distinct enough)
  • Spacing inconsistent (mix of 12px, 20px, 35px - use 8px scale)
🎯 RECOMMENDED FIXES:
  1. Increase body font-size to 16px, line-height to 1.6
  2. Darken text color from #666 to #333 for better contrast
  3. Make H1 48px, H2 32px, H3 24px for clear hierarchy
  4. Standardize spacing to 16/24/32/48px scale
Step 4: Compare to world-class examples Always reference specific sites:
"Your current design uses 14px body text. Compare to:
  • Vercel Docs: 16px with 1.7 line-height
  • Stripe Docs: 16px with 1.6 line-height
  • Your site needs larger text for readability."

CRITICAL WORKFLOW - Follow these steps in order:
  1. Auto-detect dev servers - For localhost testing, ALWAYS run server detection FIRST:
  2. cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"

    - If 1 server found: Use it automatically, inform user - If multiple servers found: Ask user which one to test - If no servers found: Ask for URL or offer to help start dev server

  3. Write scripts to /tmp - NEVER write test files to skill directory; always use /tmp/playwright-test-.js
  4. Use visible browser by default - Always use headless: false unless user specifically requests headless mode
  5. Parameterize URLs - Always make URLs configurable via environment variable or constant at top of script

How It Works

  1. You describe what you want to test/automate
  2. I auto-detect running dev servers (or ask for URL if testing external site)
  3. I write custom Playwright code in /tmp/playwright-test-.js (won't clutter your project)
  4. I execute it via: cd $SKILL_DIR && node run.js /tmp/playwright-test-.js
  5. Results displayed in real-time, browser window visible for debugging
  6. Test files auto-cleaned from /tmp by your OS

Setup (First Time)

cd $SKILL_DIR
npm run setup

This installs Playwright and Chromium browser. Only needed once.

Execution Pattern

Step 1: Detect dev servers (for localhost testing)
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))"
Step 2: Write test script to /tmp with URL parameter
// /tmp/playwright-test-page.js
const { chromium } = require('playwright');

// Parameterized URL (detected or user-provided) const TARGET_URL = 'http://localhost:3001'; // <-- Auto-detected or from user

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

await page.goto(TARGET_URL); console.log('Page loaded:', await page.title());

await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true }); console.log('📸 Screenshot saved to /tmp/screenshot.png');

await browser.close(); })();

Step 3: Execute from skill directory
cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js

Common Patterns

Test a Page (Multiple Viewports)

// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => { const browser = await chromium.launch({ headless: false, slowMo: 100 }); const page = await browser.newPage();

// Desktop test await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto(TARGET_URL); console.log('Desktop - Title:', await page.title()); await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });

// Mobile test await page.setViewportSize({ width: 375, height: 667 }); await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });

await browser.close(); })();

Test Login Flow

// /tmp/playwright-test-login.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

await page.goto(${TARGET_URL}/login);

await page.fill('input[name="email"]', '[email protected]'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]');

// Wait for redirect await page.waitForURL('/dashboard'); console.log('✅ Login successful, redirected to dashboard');

await browser.close(); })();

Fill and Submit Form

// /tmp/playwright-test-form.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => { const browser = await chromium.launch({ headless: false, slowMo: 50 }); const page = await browser.newPage();

await page.goto(${TARGET_URL}/contact);

await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', '[email protected]'); await page.fill('textarea[name="message"]', 'Test message'); await page.click('button[type="submit"]');

// Verify submission await page.waitForSelector('.success-message'); console.log('✅ Form submitted successfully');

await browser.close(); })();

Check for Broken Links

const { chromium } = require('playwright');

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

await page.goto('http://localhost:3000');

const links = await page.locator('a[href^="http"]').all(); const results = { working: 0, broken: [] };

for (const link of links) { const href = await link.getAttribute('href'); try { const response = await page.request.head(href); if (response.ok()) { results.working++; } else { results.broken.push({ url: href, status: response.status() }); } } catch (e) { results.broken.push({ url: href, error: e.message }); } }

console.log(✅ Working links: ${results.working}); console.log(❌ Broken links:, results.broken);

await browser.close(); })();

Take Screenshot with Error Handling

const { chromium } = require('playwright');

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

try { await page.goto('http://localhost:3000', { waitUntil: 'networkidle', timeout: 10000, });

await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true, });

console.log('📸 Screenshot saved to /tmp/screenshot.png'); } catch (error) { console.error('❌ Error:', error.message); } finally { await browser.close(); } })();

Test Responsive Design

// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();

const viewports = [ { name: 'Desktop', width: 1920, height: 1080 }, { name: 'Tablet', width: 768, height: 1024 }, { name: 'Mobile', width: 375, height: 667 }, ];

for (const viewport of viewports) { console.log( Testing ${viewport.name} (${viewport.width}x${viewport.height}), );

await page.setViewportSize({ width: viewport.width, height: viewport.height, });

await page.goto(TARGET_URL); await page.waitForTimeout(1000);

await page.screenshot({ path: /tmp/${viewport.name.toLowerCase()}.png, fullPage: true, }); }

console.log('✅ All viewports tested'); await browser.close(); })();

Inline Execution (Simple Tasks)

For quick one-off tasks, you can execute code inline without creating files:

# Take a quick screenshot
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"
When to use inline vs files:
  • Inline: Quick one-off tasks (screenshot, check if element exists, get page title)
  • Files*: Complex tests, responsive design checks, anything user might want to re-run

Available Helpers

Optional utility functions in lib/helpers.js:

const helpers = require('./lib/helpers');

// Detect running dev servers (CRITICAL - use this first!) const servers = await helpers.detectDevServers(); console.log('Found servers:', servers);

// Safe click with retry await helpers.safeClick(page, 'button.submit', { retries: 3 });

// Safe type with clear await helpers.safeType(page, '#username', 'testuser');

// Take timestamped screenshot await helpers.takeScreenshot(page, 'test-result');

// Handle cookie banners await helpers.handleCookieBanner(page);

// Extract table data const data = await helpers.extractTableData(page, 'table.results');

// Compress image for Claude Code upload (auto-fixes oversized images) const result = await helpers.compressImage(); // Auto-detects latest screenshot // Or with specific path: const result = await helpers.compressImage('/path/to/image.png', { maxDimension: 3500, outputPath: '/tmp/compressed.png' });

See lib/helpers.js for full list.

Image Compression for Claude Code

Auto-compress oversized images that exceed Claude's 8000px dimension limit.

Quick Start

# Install sharp dependency (one-time)
cd $SKILL_DIR && npm install sharp

Auto-compress latest screenshot

cd $SKILL_DIR && node -e "require('./lib/helpers').compressImage().then(r => console.log(JSON.stringify(r, null, 2)))"

Usage in Scripts

const helpers = require('./lib/helpers');

// Auto-detect and compress latest screenshot const result = await helpers.compressImage(); if (result.success) { console.log(Compressed to: ${result.outputPath}); console.log(Original: ${result.originalDimensions.width}x${result.originalDimensions.height}); console.log(Size reduction: ${((1 - result.finalSize/result.originalSize) 100).toFixed(1)}%); }

// Compress specific file await helpers.compressImage('/Users/xiaoyinqu/Desktop/screenshot.png', { maxDimension: 3500, // Max width/height (default: 3500) outputPath: '/tmp/compressed.png' // Output location });

How It Works

  1. Auto-detects latest image - Searches Desktop/Downloads for recent screenshots
  2. Smart compression - Resizes to max 3500px (safe margin under 8000px limit)
  3. Handles Unicode filenames - Works with macOS screenshots (narrow no-break space \u202f)
  4. Preserves quality - Uses sharp's high-quality resampling
  5. Returns stats - Original/final dimensions and file sizes

Example Output

📸 Processing: Screenshot 2026-03-01 at 2.33.31 AM.png
📏 Original: 2298x1694 px (666.7 KB)
✅ Already within size limits
💾 Saved: /tmp/compressed_screenshot.png
📊 Final size: 485.8 KB (72.9% of original)

Return Value

{
  success: true,
  inputPath: '/Users/xiaoyinqu/Desktop/Screenshot...',
  outputPath: '/tmp/compressed_screenshot.png',
  originalDimensions: { width: 2298, height: 1694 },
  originalSize: 666.7,  // KB
  finalSize: 485.8,     // KB
  resized: false        // true if dimensions were reduced
}

Custom HTTP Headers

Configure custom headers for all HTTP requests via environment variables. Useful for:

  • Identifying automated traffic to your backend
  • Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML)
  • Adding authentication tokens globally

Configuration

Single header (common case):
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
  cd $SKILL_DIR && node run.js /tmp/my-script.js
Multiple headers (JSON format):
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
  cd $SKILL_DIR && node run.js /tmp/my-script.js

How It Works

Headers are automatically applied when using helpers.createContext():

const context = await helpers.createContext(browser);
const page = await context.newPage();
// All requests from this page include your custom headers

For scripts using raw Playwright API, use the injected getContextOptionsWithHeaders():

const context = await browser.newContext(
  getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),
);

Advanced Usage

For comprehensive Playwright API documentation, see API_REFERENCE.md:

  • Selectors & Locators best practices
  • Network interception & API mocking
  • Authentication & session management
  • Visual regression testing
  • Mobile device emulation
  • Performance testing
  • Debugging techniques
  • CI/CD integration

Tips

  • 🚨 AUTO-FIX: Image upload errors - If user gets "8000 pixels" error, immediately run compressImage() and display result (NEVER ask)
  • CRITICAL: Detect servers FIRST - Always run detectDevServers() before writing test code for localhost testing
  • Custom headers - Use PW_HEADER_NAME/PW_HEADER_VALUE env vars to identify automated traffic to your backend
  • Use /tmp for test files - Write to /tmp/playwright-test-*.js, never to skill directory or user's project
  • Parameterize URLs - Put detected/provided URL in a TARGET_URL constant at the top of every script
  • DEFAULT: Visible browser - Always use headless: false unless user explicitly asks for headless mode
  • Headless mode - Only use headless: true when user specifically requests "headless" or "background" execution
  • Slow down: Use slowMo: 100 to make actions visible and easier to follow
  • Wait strategies: Use waitForURL, waitForSelector, waitForLoadState instead of fixed timeouts
  • Error handling: Always use try-catch for robust automation
  • Console output: Use console.log() to track progress and show what's happening

Troubleshooting

Image upload error ("8000 pixels exceeded"): AUTO-FIX (execute immediately, no questions):
cd $SKILL_DIR && node -e "require('./lib/helpers').compressImage()"
Then use Read tool on /tmp/compressed_screenshot.png. Tell user "图片太大,帮你压缩了". Playwright not installed:
cd $SKILL_DIR && npm run setup
Image compression not working:
cd $SKILL_DIR && npm install sharp
Module not found: Ensure running from skill directory via run.js wrapper Browser doesn't open: Check headless: false and ensure display available Element not found: Add wait: await page.waitForSelector('.element', { timeout: 10000 })

Example Usage

User: "Test if the marketing page looks good"

Claude: I'll test the marketing page across multiple viewports. Let me first detect running servers... [Runs: detectDevServers()] [Output: Found server on port 3001] I found your dev server running on http://localhost:3001

[Writes custom automation script to /tmp/playwright-test-marketing.js with URL parameterized] [Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-marketing.js] [Shows results with screenshots from /tmp/]

User: "Check if login redirects correctly"

Claude: I'll test the login flow. First, let me check for running servers... [Runs: detectDevServers()] [Output: Found servers on ports 3000 and 3001] I found 2 dev servers. Which one should I test?

  • http://localhost:3000
  • http://localhost:3001
User: "Use 3001"

[Writes login automation to /tmp/playwright-test-login.js] [Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-login.js] [Reports: ✅ Login successful, redirected to /dashboard]

Notes

  • Each automation is custom-written for your specific request
  • Not limited to pre-built scripts - any browser task possible
  • Auto-detects running dev servers to eliminate hardcoded URLs
  • Test scripts written to /tmp for automatic cleanup (no clutter)
  • Code executes reliably with proper module resolution via run.js
  • Progressive disclosure - API_REFERENCE.md loaded only when advanced features needed