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