playwright について
`playwright-core` と Chrome DevTools Protocol を使って Chromium を自動化するスキルです。`.mjs` ファイルでブラウザー操作を記述する用途向けに、ページ操作、検査、監視のパターンを提供します。
主な機能
- 01CDP 経由で Chromium に接続
- 02スクリーンショット撮影とページ検査
- 03フォーム入力と要素のクリック
- 04テキスト、HTML、タイトルの抽出
- 05コンソールとネットワーク応答の監視
- 06ブラウザーセッションの動画記録
活用シーン
- 01複数ページにわたるログインの自動化
- 02描画ページとスクリーンショットの検査
- 03スクリプトによるページ内容の収集
- 04ブラウザーリクエストとコンソールエラーの調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ fellowship-dev/ dogfooded-skills --skill playwrightインストール後、Claude Code で/playwrightを入力して読み込まれたか確認します。
詳細
Playwright による Web 自動化
Chromium と playwright-core を CDP 経由で使用したブラウザー自動化です。MCP サーバーは不要で、スクリプトは .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
CDP で 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"
Playwright で接続する(ESM)
スクリプトは .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
重要: 必ず .mjs 拡張子を使用してください。playwright-core は ESM 専用です。
主なパターン
スクリーンショットと検査
await 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"]', '[email protected]');
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")')
]);
SPA/クライアント側でレンダリングされるデータ
SPA はヘッドレス環境では完全にハイドレーションされない場合があります。利用可能であれば、データエンドポイントを使用してください。
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', '[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' });
動画の記録
// 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
コンソールとネットワークの監視
page.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")') - CSS —
page.click('.submit-btn')(壊れやすいため、最後の手段)
クリーンアップ
pkill -f "remote-debugging-port=9222"
トラブルシューティング
| 問題 | 解決策 |
|---|---|
| CDP が応答しない | しばらく待ってから、curl http://localhost:9222/json/version を確認します |
| ポート 9222 が使用中 | pkill -f "remote-debugging-port=9222" を実行してから再起動します |
| ESM のインポートエラー | .js ではなく .mjs 拡張子を使用します |
| ページが読み込まれない | タイムアウトを延長し、waitUntil: 'domcontentloaded' を使用します |
| スクリーンショットが真っ白 | キャプチャの前に page.waitForTimeout(2000) を追加します |
| CSS が読み込まれない | waitUntil: 'networkidle' を使用するか、表示される要素を待ちます |
| フォーム送信に失敗する | 要素が表示されているか、await el.isVisible() で確認します |
コメント
Markdown 対応