mcpField
P
playwright-screenshot-inspector

playwright-screenshot-inspector

github.com/curiositechテスト・品質257251 回インストール2 日前に更新
リポジトリ

playwright-screenshot-inspector について

Web ページの自動ビジュアル検証を行う開発者向けの Playwright スキルです。React の描画完了を待ってから、ピクセル比較だけでなくマルチモーダル AI でスクリーンショットを意味的に検査します。

主な機能

  1. 01ヘッドレス Playwright によるスクリーンショット撮影
  2. 02React コンテンツの描画待ち
  3. 03コンテンツの読み込みとレイアウト状態の確認
  4. 04ダーク・ライトテーマの表示検証
  5. 05スクリーンショット比較と差分評価
  6. 06可読性と視覚的アクセシビリティの確認

活用シーン

  1. 01ページの撮影結果が空白や読み込み中でないか確認
  2. 02ダーク・ライトテーマが正しく適用されるか検証
  3. 03UI の変更が意図的か不具合かを判別

MCP 対応

対話的なブラウザー操作には Playwright MCP、自動テストスイートには標準の Python Playwright を推奨しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill playwright-screenshot-inspector

インストール後、Claude Code で/playwright-screenshot-inspectorを入力して読み込まれたか確認します。

同梱ファイル.claude-plugin/plugin.json

詳細

Playwright スクリーンショットインスペクター

Playwright とマルチモーダル AI による検査を使用して、スクリーンショットの自動取得、分析、UI 検証を行う LLM 搭載のビジュアルテストエキスパートです。

アクティベーションのトリガー

次の場合にアクティベートします:

  • 「screenshot test」、「visual test」、「screenshot inspection」
  • 「playwright headless」、「playwright screenshot」
  • 「UI verification」、「visual regression」
  • 「theme compliance test」、「dark mode test」、「light mode test」
  • 「automated screenshot」、「capture and analyze」
  • 「compare screenshots」、「visual diff」

次の場合には使用しません:

  • 単発の簡単なスクリーンショット(ブラウザーの DevTools を使用してください)
  • AI を使わないピクセル単位の比較(Playwright 標準の toHaveScreenshot を使用してください)
  • Web 以外の UI テスト(プラットフォーム固有のツールを使用してください)
  • パフォーマンステスト(Lighthouse/WebPageTest を使用してください)

基本方針

従来のビジュアルテストはピクセルを比較します。LLM 搭載のビジュアルテストは意味を理解します。

「この 50 ピクセルが変わった」ではなく、LLM による検査では次のような質問に答えます:

  • 「コンテンツは実際にレンダリングされていますか?」
  • 「テーマは正しく切り替わりますか?」
  • 「操作可能な要素は表示され、適切にスタイル設定されていますか?」
  • 「何が壊れていて、何が単に異なっているだけですか?」

スクリーンショット検査のループ

┌─────────────────────────────────────────────────────────────┐
│                    LLM SCREENSHOT INSPECTION                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. CAPTURE (Playwright)                                    │
│     └─► Wait for React hydration, not just network          │
│                                                             │
│  2. READ (Claude vision)                                    │
│     └─► Pass screenshot to LLM with specific questions      │
│                                                             │
│  3. ANALYZE (Structured response)                           │
│     └─► Extract: content present? theme correct? errors?    │
│                                                             │
│  4. ACT (Conditional logic)                                 │
│     └─► Pass/fail based on semantic understanding           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

重要:React コンテンツの表示を待つ

最もよくある失敗:React のハイドレーション前にスクリーンショットを撮影することです。

アンチパターン:ネットワークアイドル状態だけを待つ
# ❌ WRONG - React may not have rendered yet
page.goto(url)
page.wait_for_load_state('networkidle')
page.screenshot(path='broken.png')  # Often blank!
正しいパターン:実際のコンテンツを待つ
# ✅ CORRECT - Wait for React to mount
page.goto(url, wait_until='domcontentloaded')
page.wait_for_load_state('networkidle')

# Give React time to hydrate
import time
time.sleep(0.5)

# Wait for actual content selector
page.wait_for_selector('.main-content, h1, [data-testid="app"]',
                       state='visible',
                       timeout=10000)

# Verify content exists
body_text = page.locator('body').inner_text()
if len(body_text) < 50:
    time.sleep(2)  # Extra wait for slow hydration

page.screenshot(path='good.png', full_page=True)
コンテンツ検証関数
def wait_for_react_content(page, selectors, timeout=10000):
    """Wait for React to hydrate by checking for actual content."""
    page.wait_for_load_state('domcontentloaded')
    page.wait_for_load_state('networkidle')
    time.sleep(0.5)  # React hydration buffer

    for selector in selectors.split(','):
        try:
            locator = page.locator(selector.strip())
            if locator.count() > 0:
                locator.first.wait_for(state='visible', timeout=timeout)
                return True
        except:
            continue

    # Fallback: wait for substantial body content
    try:
        page.wait_for_function(
            'document.body.innerText.length > 100',
            timeout=timeout
        )
        return True
    except:
        return False

ヘッドレスモード:ウィンドウの大量起動を防ぐ

ブラウザーウィンドウが起動しないように、常に headless=True を使用してください:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # CRITICAL: headless=True prevents visible browser windows
    browser = p.chromium.launch(headless=True)

    context = browser.new_context(
        viewport={'width': 1280, 'height': 800},
        color_scheme='dark'  # Initial theme
    )
    page = context.new_page()

    # ... your test logic ...

    browser.close()  # Always clean up
テーマテストのパターン
# Dark mode screenshot
page.emulate_media(color_scheme='dark')  # Note: on PAGE, not context
page.goto(url)
wait_for_react_content(page, '.app-container, main, h1')
page.screenshot(path='dark.png', full_page=True)

# Light mode screenshot
page.emulate_media(color_scheme='light')
page.reload()
wait_for_react_content(page, '.app-container, main, h1')
page.screenshot(path='light.png', full_page=True)

LLM によるスクリーンショット分析のパターン

パターン 1:コンテンツの検証
Prompt: "Analyze this screenshot. Answer:
1. Is the main content rendered (not blank/loading)?
2. What major UI elements are visible?
3. Are there any error states or broken layouts?
4. Rate content completeness: FULL / PARTIAL / EMPTY"
パターン 2:テーマ準拠の確認
Prompt: "This is a {dark/light} mode screenshot. Verify:
1. Background color matches expected theme (dark bg for dark mode)
2. Text has sufficient contrast against background
3. Interactive elements are visible and styled correctly
4. No theme leakage (dark elements on light bg or vice versa)"
パターン 3:比較分析
Prompt: "Compare these two screenshots (before/after). Identify:
1. What changed between them?
2. Are changes intentional (theme switch) or bugs?
3. Is any content missing in the 'after' version?
4. Rate similarity: IDENTICAL / MINOR_DIFF / MAJOR_DIFF / BROKEN"
パターン 4:アクセシビリティチェック
Prompt: "Evaluate this screenshot for visual accessibility:
1. Is text readable (sufficient size and contrast)?
2. Are interactive elements clearly identifiable?
3. Is there visual hierarchy (headings, sections)?
4. Any elements that would fail WCAG contrast requirements?"

テストスクリプトの完全なテンプレート

#!/usr/bin/env python3
"""
LLM-Powered Screenshot Test Suite
Captures screenshots and uses Claude vision for semantic analysis.
"""

from playwright.sync_api import sync_playwright
import os
import time

PAGES_TO_TEST = [
    # (path, name, content_selectors)
    ('/', 'Home', '.hero, main, h1'),
    ('/about', 'About', '.about-content, main, h1'),
    ('/dashboard', 'Dashboard', '.dashboard, .stats, h1'),
]

BASE_URL = 'http://localhost:5173'
SCREENSHOT_DIR = '/tmp/visual-tests'


def wait_for_content(page, selectors, timeout=10000):
    """Wait for React/Vue/Svelte to hydrate."""
    page.wait_for_load_state('domcontentloaded')
    page.wait_for_load_state('networkidle')
    time.sleep(0.5)

    for selector in selectors.split(','):
        try:
            loc = page.locator(selector.strip())
            if loc.count() > 0:
                loc.first.wait_for(state='visible', timeout=timeout)
                return True
        except:
            continue

    try:
        page.wait_for_function('document.body.innerText.length > 100', timeout=timeout)
        return True
    except:
        return False


def capture_themed_screenshots(page, url, name, selectors):
    """Capture both dark and light mode screenshots."""
    safe_name = name.lower().replace(' ', '-')
    results = {'name': name, 'url': url}

    for theme in ['dark', 'light']:
        page.emulate_media(color_scheme=theme)

        if theme == 'dark':
            page.goto(url, wait_until='domcontentloaded')
        else:
            page.reload(wait_until='domcontentloaded')

        content_loaded = wait_for_content(page, selectors)

        if not content_loaded:
            print(f"  ⚠️  {theme} mode: Content slow to load, waiting...")
            time.sleep(2)

        screenshot_path = f'{SCREENSHOT_DIR}/{safe_name}-{theme}.png'
        page.screenshot(path=screenshot_path, full_page=True)

        # Check content length
        body_text = page.locator('body').inner_text().strip()
        results[f'{theme}_screenshot'] = screenshot_path
        results[f'{theme}_content_length'] = len(body_text)
        results[f'{theme}_has_content'] = len(body_text) > 50

        print(f"  {theme}: {'✅' if results[f'{theme}_has_content'] else '❌'} ({len(body_text)} chars)")

    return results


def run_tests():
    """Run visual tests on all pages."""
    os.makedirs(SCREENSHOT_DIR, exist_ok=True)

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            viewport={'width': 1280, 'height': 800},
            color_scheme='dark'
        )
        page = context.new_page()

        # Capture console errors
        errors = []
        page.on('console', lambda m: errors.append(m.text) if m.type == 'error' else None)

        results = []

        for path, name, selectors in PAGES_TO_TEST:
            print(f"Testing {name}...")
            url = f'{BASE_URL}{path}'
            result = capture_themed_screenshots(page, url, name, selectors)
            result['errors'] = list(errors)
            errors.clear()
            results.append(result)

        browser.close()

        # Summary
        print("\n" + "=" * 50)
        print("VISUAL TEST SUMMARY")
        print("=" * 50)

        passed = sum(1 for r in results
                     if r.get('dark_has_content') and r.get('light_has_content'))
        print(f"\nPassed: {passed}/{len(results)}")
        print(f"Screenshots: {SCREENSHOT_DIR}")

        return results


if __name__ == '__main__':
    run_tests()

MCP と Playwright 標準機能の選択フローチャート

What are you doing?
│
├─ Interactive debugging / exploring
│  └─► Playwright MCP (see live browser)
│
├─ Automated test suite
│  └─► Native Python Playwright (headless)
│
├─ CI/CD pipeline
│  └─► Native Python Playwright (headless)
│
├─ Screenshot capture for LLM analysis
│  └─► Native Python Playwright (headless)
│
└─ One-off inspection
   └─► Either works, MCP is convenient

よくある失敗と修正方法

失敗:スクリーンショットが真っ白

原因:React のハイドレーション前にスクリーンショットを撮影しています 修正方法:コンテンツセレクターを待ち、ハイドレーション用の待機時間を追加します

失敗:「Reconnecting...」バッジが表示される

原因:HMR/WebSocket が接続されていません(テストでは見た目だけの問題です) 修正方法:問題ない場合が多いため、実際のコンテンツに注目します

失敗:テーマが適用されない

原因:emulate_media をページではなくコンテキストで呼び出しています 修正方法:page.emulate_media(color_scheme='dark') を使用します

失敗:ブラウザーウィンドウが起動する

原因:headless=False を指定しているか、標準機能の代わりに MCP を使用しています 修正方法:p.chromium.launch(headless=True) を使用します

失敗:コンテンツの表示がタイムアウトする

原因:セレクターが間違っているか、ページ自体に問題があります 修正方法:セレクターが存在することを確認し、コンソールエラーを確認します


Claude Code との統合

Claude がこのパターンで取得したスクリーンショットを読み取る場合:

  1. 具体的な分析を依頼する:スクリーンショットを見せるだけでなく、対象を絞った質問をします
  2. コンテキストを伝える:「これはダークモードです」や「これはログインページです」
  3. 体系的に比較する:変更前と変更後、ダークとライト、デスクトップとモバイル
  4. 意味的な分析を信頼する:LLM は「空白のページ」と「コンテンツが読み込まれたページ」を見分けられます

参考資料

研究論文
ツールと統合
公式ドキュメント

バージョン履歴

  • 2026-01-23:スキルを初めて作成
    • マルチモーダル LLM によるスクリーンショット分析のベストプラクティスを調査
    • React のハイドレーション完了を待つパターンを文書化
    • ヘッドレスモードの要件を追加
    • テストスクリプトの完全なテンプレートを作成

主なポイント:役に立たないスクリーンショットテストと役に立つスクリーンショットテストの違いは、ネットワークだけでなくコンテンツを待つことです。LLM はセマンティクスを分析できますが、分析するコンテンツが実際に存在する場合に限ります。

コメント