mcpField
P
playwright

playwright

github.com/fellowship-devDevelopment3178 installsUpdated 3w ago
Repository

About playwright

A skill for automating Chromium through `playwright-core` and the Chrome DevTools Protocol. It is intended for browser tasks scripted as `.mjs` files and includes patterns for page interaction, inspection, and monitoring.

Key features

  1. 01Connect to Chromium over CDP
  2. 02Capture screenshots and inspect pages
  3. 03Fill forms and click elements
  4. 04Extract text, HTML, and titles
  5. 05Monitor console and network responses
  6. 06Record browser sessions as video

Use cases

  1. 01Automating multi-page login flows
  2. 02Inspecting rendered pages and screenshots
  3. 03Collecting page content for scripts
  4. 04Troubleshooting browser requests and console errors

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/fellowship-dev/dogfooded-skills --skill playwright

After installing, type/playwrightin Claude Code to confirm it loaded.

Details

From the project README

Playwright Web Automation

Browser automation via Chromium + playwright-core over CDP. No MCP server needed — scripts run as .mjs files.

Prerequisites

# Install playwright-core (no browser download — uses existing Chromium)
npm install playwright-core

# Verify Chromium exists
which chromium || ls /Applications/Chromium.app/Contents/MacOS/Chromium

Launch Chromium with CDP

# Kill any existing debug instance
pkill -f "remote-debugging-port=9222" 2>/dev/null

# Launch headless (cron/CI) or headed (interactive)
chromium --remote-debugging-port=9222 \
  --user-data-dir="/tmp/playwright-profile" \
  --disable-blink-features=AutomationControlled \
  --no-first-run --no-default-browser-check \
  --headless=new \
  "about:blank" > /tmp/chromium.log 2>&1 &

# macOS: use full path /Applications/Chromium.app/Contents/MacOS/Chromium
# Linux: chromium-browser or chromium

# Wait for CDP
sleep 3 && curl -sf http://localhost:9222/json/version > /dev/null && echo "CDP ready"

Connect via Playwright (ESM)

Write scripts as .mjs files:

// run: node script.mjs
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP('http://localhost:9222');
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || await ctx.newPage();

await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 15000 });
await page.waitForTimeout(2000);
await page.screenshot({ path: '/tmp/screenshot.png' });

await browser.close(); // disconnects, doesn't kill Chromium

IMPORTANT: Always use .mjs extension — playwright-core is ESM-only.

Key Patterns

Screenshot & Inspect
await page.screenshot({ path: '/tmp/screen.png' });
// Read /tmp/screen.png in Claude to see it visually
Fill Forms & Click
// Inspect inputs first
const inputs = await page.evaluate(() =>
  Array.from(document.querySelectorAll('input')).map(i => ({
    name: i.name, type: i.type, placeholder: i.placeholder
  }))
);
console.log(JSON.stringify(inputs, null, 2));

await page.fill('input[name="email"]', '[email protected]');
await page.click('button:has-text("Submit")');
await page.waitForTimeout(2000);
Extract Page Content
const text = await page.evaluate(() => document.body.innerText);
const html = await page.content();
const title = await page.title();
Wait for Navigation
await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle' }),
  page.click('button:has-text("Login")')
]);
SPA / Client-Side Rendered Data

SPAs may not hydrate fully in headless. Use data endpoints if available:

const data = await page.evaluate(async () => {
  const res = await fetch('/api/data', { credentials: 'include' });
  return res.json();
});
Multi-Page Flow
await page.goto('https://app.example.com/login');
await page.fill('#email', '[email protected]');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
await page.screenshot({ path: '/tmp/after-login.png' });

// Continue to next page
await page.click('a:has-text("Dashboard")');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: '/tmp/dashboard.png' });
Record Video
// Must create a NEW context for video recording
const ctx = await browser.newContext({
  recordVideo: { dir: '/tmp/videos/', size: { width: 1280, height: 720 } }
});
const page = await ctx.newPage();
await page.goto('https://example.com');
// ... do things ...
await ctx.close(); // video saved to /tmp/videos/*.webm
Console & Network Monitoring
page.on('console', msg => console.log(`[${msg.type()}] ${msg.text()}`));
page.on('response', res => {
  if (res.status() >= 400) console.log(`[${res.status()}] ${res.url()}`);
});

Selectors (prefer in this order)

  1. data-testid="foo" — page.click('[data-testid="foo"]')
  2. Role — page.getByRole('button', { name: 'Submit' })
  3. Text — page.click('button:has-text("Submit")')
  4. CSS — page.click('.submit-btn') (fragile, last resort)

Cleanup

pkill -f "remote-debugging-port=9222"

Troubleshooting

ProblemSolution
CDP not respondingWait longer, check curl http://localhost:9222/json/version
Port 9222 in usepkill -f "remote-debugging-port=9222" then relaunch
ESM import errorUse .mjs extension, not .js
Page not loadingIncrease timeout, use waitUntil: 'domcontentloaded'
Screenshots blankAdd page.waitForTimeout(2000) before capture
CSS not loadedUse waitUntil: 'networkidle' or wait for a visible element
Form submit failsCheck if element is visible: await el.isVisible()

Comments

Markdown supported