Loading...
Loading...
Use when automating a browser with Playwright CLI over CDP — navigate, screenshot, fill forms, or extract data.
npx skill4agent add fellowship-dev/dogfooded-skills playwright.mjs# 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# 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".mjs// 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.mjsawait page.screenshot({ path: '/tmp/screen.png' });
// Read /tmp/screen.png in Claude to see it visually// 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"]', 'user@example.com');
await page.click('button:has-text("Submit")');
await page.waitForTimeout(2000);const text = await page.evaluate(() => document.body.innerText);
const html = await page.content();
const title = await page.title();await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.click('button:has-text("Login")')
]);const data = await page.evaluate(async () => {
const res = await fetch('/api/data', { credentials: 'include' });
return res.json();
});await page.goto('https://app.example.com/login');
await page.fill('#email', 'user@example.com');
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' });// 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/*.webmpage.on('console', msg => console.log(`[${msg.type()}] ${msg.text()}`));
page.on('response', res => {
if (res.status() >= 400) console.log(`[${res.status()}] ${res.url()}`);
});data-testid="foo"page.click('[data-testid="foo"]')page.getByRole('button', { name: 'Submit' })page.click('button:has-text("Submit")')page.click('.submit-btn')pkill -f "remote-debugging-port=9222"| Problem | Solution |
|---|---|
| CDP not responding | Wait longer, check |
| Port 9222 in use | |
| ESM import error | Use |
| Page not loading | Increase timeout, use |
| Screenshots blank | Add |
| CSS not loaded | Use |
| Form submit fails | Check if element is visible: |