playwright-skill について
Playwright を使ったウェブテストと自動化のためのスキルです。カスタム JavaScript を作成し、サーバー管理からページ確認、操作、検証までを進めます。
主な機能
- 01JavaScript によるブラウザー自動化
- 02テストサーバーの確認と起動
- 03操作前の DOM 調査
- 04フォームとナビゲーションのテスト
- 05スクリーンショットとネットワーク確認
活用シーン
- 01ログインやフォームのフローをテスト
- 02複数のビューポートでレイアウトを確認
- 03リンク切れや画像の読み込み漏れを検出
- 04ウェブページからのデータ抽出を自動化
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dedalus-erp-pas/ foundation-skills --skill playwright-skillインストール後、Claude Code で/playwright-skillを入力して読み込まれたか確認します。
詳細
Playwright による Web テストと自動化
Playwright を使用した包括的な Web テストスキルです。あらゆるテストや自動化タスクに対応するカスタム JavaScript コードを作成できます。
前提条件
- Node.js:必須です — https://nodejs.org からインストールしてください
- playwright npm パッケージ:
npm install playwright(ヘッドレス Chromium が自動的にインストールされます)
判断ツリー:アプローチの選択
User task → Is server already running?
├─ Yes → Direct Testing
│ ├─ Static HTML? → Navigate directly (file:// or http://)
│ └─ Dynamic webapp? → Use Reconnaissance-Then-Action pattern
│
└─ No → Server Management Required
├─ Single server → Start server, then test
└─ Multiple servers → Start all servers, coordinate testing
重要なワークフロー
- サーバーが実行中か確認する - 実行中の開発サーバーを検出するか、必要に応じてサーバーを起動します
- スクリプトを /tmp に書き込む - テストファイルをスキルディレクトリに書き込まないでください。必ず
/tmp/playwright-test-*.jsを使用します - デフォルトで表示ブラウザーを使用する - ユーザーからヘッドレスモードを明示的に指定された場合を除き、常に
headless: falseを使用します - 動的コンテンツを待つ - 動的な Web アプリを調査する前に、
waitForLoadState('networkidle')を使用します - URL をパラメーター化する - スクリプトの先頭にある定数で、常に URL を設定できるようにします
調査してから操作するパターン
DOM の構造が事前に分からない動的な Web アプリの場合:
// /tmp/playwright-test-reconnaissance.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3000';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// STEP 1: Navigate and wait for dynamic content
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle'); // CRITICAL for dynamic apps
// STEP 2: Reconnaissance - discover what's on the page
await page.screenshot({ path: '/tmp/inspect.png', fullPage: true });
const buttons = await page.locator('button').all();
console.log(`Found ${buttons.length} buttons`);
for (let i = 0; i < buttons.length; i++) {
const text = await buttons[i].textContent();
console.log(` Button ${i}: "${text}"`);
}
// STEP 3: Action - interact with discovered elements
const loginButton = page.locator('button:has-text("Login")');
if (await loginButton.isVisible()) {
await loginButton.click();
console.log('✅ Clicked login button');
}
await browser.close();
})();
サーバー管理
実行中のサーバーを確認する
# Check if port is in use
lsof -i :3000 # Mac/Linux
netstat -ano | findstr :3000 # Windows
テスト前にサーバーを起動する
// /tmp/playwright-test-with-server.js
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const TARGET_URL = 'http://localhost:3000';
(async () => {
// Start server
console.log('Starting server...');
const server = spawn('npm', ['run', 'dev'], { shell: true });
server.stdout.on('data', (data) => console.log(`Server: ${data}`));
server.stderr.on('data', (data) => console.error(`Server Error: ${data}`));
// Wait for server to be ready
await new Promise(resolve => setTimeout(resolve, 3000));
// Run tests
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
// Your test logic here
console.log('Title:', await page.title());
await browser.close();
// Clean up server
server.kill();
console.log('✅ Tests complete, server stopped');
})();
よくあるパターン
ページをテストする(複数のビューポート)
// /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();
})();
ログインフローをテストする
// /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();
})();
フォームに入力して送信する
// /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();
})();
リンク切れを確認する
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();
})();
エラー処理付きでスクリーンショットを撮る
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();
}
})();
レスポンシブデザインをテストする
// /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})`,
);
& Discovery
```javascript
// Check visibility
const isVisible = await page.locator('button').isVisible();
// Get text
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
// Find all elements
const allButtons = await page.locator('button').all();
const allLinks = await page.locator('a').all();
// Check if element exists
const count = await page.locator('.modal').count();
console.log(`Found ${count} modals`);
ネットワークとコンソール
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
// Capture console logs
page.on('console', msg => {
console.log(`Browser console [${msg.type()}]:`, msg.text());
});
ベストプラクティス
✅ 推奨事項
- 動的アプリでは networkidle を待つ - SPA/React/Vue アプリの DOM を調べる前に、必ず
page.waitForLoadState('networkidle')を使用します - 調査パターンを使用する - スクリーンショットを撮り、DOM を調べてから、見つかった内容に基づいて操作します
- デフォルトで表示ブラウザーを使用する - デバッグしやすくするため、
headless: falseを使用します - 分かりやすいセレクター - 壊れやすい CSS セレクターよりも、
text=、role=、データ属性を使用します - エラー処理 - 自動化処理を try-catch ブロックで囲みます
- リソースをクリーンアップする - 必ずブラウザーを閉じ、サーバーを停止します
❌ 禁止事項
- networkidle より前に調べない - 動的な Web アプリでは、DOM の準備がまだ整っていない場合があります
- 固定タイムアウトを使用しない - 任意の待ち時間ではなく、
waitForSelector()またはwaitForLoadState()を使用します - スキルディレクトリに書き込まない - テストスクリプトには必ず
/tmpを使用します - URL をハードコードしない - 変更しやすいよう、スクリプトの先頭にある定数を使用しますeadless: false, // 表示ブラウザー slowMo: 50 // 50ms 遅くする });
const page = await browser.newPage();
// 移動 await page.goto('https://example.com', { waitUntil: 'networkidle' // ネットワークがアイドル状態になるまで待つ });
// 閉じる await browser.close();
### Selectors & Interactions
```javascript
// Click
await page.click('button.submit');
await page.dblclick('.item');
// Fill input
await page.fill('input[name="email"]', '[email protected]');
await page.getByLabel('Email').fill('[email protected]');
// Checkbox
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// Select dropdown
await page.selectOption('select#country', 'usa');
// Type with delay
await page.type('#username', 'testuser', { delay: 100 });
待機方法
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
// Wait for element
await page.waitForSelector('.success-message');
await page.waitForSelector('.spinner', { state: 'hidden' });
// Wait for timeout (use sparingly)
await page.waitForTimeout(1000);
スクリーンショット
// Full page screenshot
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true
});
// Element screenshot
await page.locator('.chart').screenshot({
path: '/tmp/chart.png'
});
```Quick Tips
- **Visible browser** - Always `headless: false` unless explicitly requested
- **Write to /tmp** - Scripts go to `/tmp/playwright-test-*.js`, never to project directories
- **Parameterize URLs** - Use `TARGET_URL` constant at top of script
- **Slow down** - Use `slowMo: 100` to see actions in real-time
- **Wait smart** - Use `waitForLoadState('networkidle')` for dynamic apps before inspecting
- **Error handling** - Wrap in try-catch with proper cleanup in finally block
- **Progress feedback** - Use `console.log()` to
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
ネットワーク
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
ヒント
- デフォルト:表示ブラウザー - ユーザーからヘッドレスモードを明示的に指定された場合を除き、常に
headless: falseを使用します - テストファイルには /tmp を使用する -
/tmp/playwright-test-*.jsに書き込みます。スキルディレクトリやユーザーのプロジェクトには書き込まないでください - URL をパラメーター化する - 検出または指定された URL を、すべてのスクリプトの先頭にある
TARGET_URL定数に設定します - 動作をゆっくりにする:
slowMo: 100を使用すると、操作が見やすく、追いやすくなります - 待機方法: 固定タイムアウトではなく、
waitForURL、waitForSelector、waitForLoadStateを使用します - エラー処理: 堅牢な自動化のため、必ず try-catch を使用します
- コンソール出力:
console.log()を使用して進行状況や実行中の処理を表示します
よくあるユースケース
ビジュアルテスト:
- さまざまなビューポートでスクリーンショットを撮る
- 視覚的な変更を比較する
- レスポンシブデザインをテストする
機能テスト:
- ログインフローをテストする
- フォームの検証
- ナビゲーションフロー
- エラー処理
検証:
- リンク切れを確認する
- 画像が読み込まれることを確認する
- ページの読み込み時間をテストする
- アクセシビリティを確認する
自動化:
- フォームに自動入力する
- ユーザーフローに沿ってクリックする
- ページからデータを抽出する
- レポートを生成する
注意事項
- 各自動化は、依頼内容に合わせて個別に作成されます
- 事前に用意されたスクリプトに限定されず、あらゆるブラウザータスクに対応できます
- 実行中の開発サーバーを自動検出し、URL のハードコーディングを不要にします
- テストスクリプトは
/tmpに作成され、自動的に削除されます(不要なファイルが残りません) - プログレッシブディスクロージャーにより、必要な場合にのみ高度なドキュメントを読み込みます
参考資料
コメント
Markdown 対応