ryokatsu.dev

Playwright MCPでサイトのアクセシビリティチェックをしてもらう


流行りのPlaywright MCPでサイトのアクセシビリティチェックをしてもらう。

雑に検証

cursorから動かした。設定画面からmcpを登録できるので登録する

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest"
      ]
    }
  }
}

cursorくん(モデルはclaude-3.7-sonnet)に、プロンプトを渡す。本来であればちゃんと指示すれば良いんだけど、雑にmcp.jsonを読み込ませて以下をお願いした

Playwright MCPを使ってWCAG2.2のA相当をclearしているかどうかをチェックしてルートディレクトリにその調査結果をマークダウンとして出力してください

すると勝手にサーバーを起動して、ポチポチPlaywrightの実行を許可していくことで実行された。 以下のマークダウンの内容を出力してくれた。

# アクセシビリティ評価レポート: ryokatsu.dev

**評価日**: 2025年4月21日  
**評価方法**: Playwright MCPを使用したアクセシビリティスナップショット分析  
**対象**: WCAG 2.2 レベルA基準  

## 評価サマリー

ryokatsu.devサイトは、WCAG 2.2のレベルA基準に対して概ね適合していますが、いくつかの改善点が見つかりました。以下に各チェック項目の結果と推奨される対応をまとめます。

## 適合している項目

### 1. ランドマーク領域 (WCAG 1.3.1 情報及び関係性)
- `<banner>` (ヘッダー)、`<main>``<contentinfo>` (フッター) の適切なランドマーク領域が使用されています。

### 2. 見出し階層 (WCAG 1.3.1 情報及び関係性)
- 見出し要素(`<h1>`, `<h2>`, `<h3>` など)が適切に使用されています。

### 3. リストの使用 (WCAG 1.3.1 情報及び関係性)
- ナビゲーションやコンテンツのリスト構造が適切に実装されています。

### 4. キーボードフォーカス (WCAG 2.1.1 キーボード)
- リンクやフォーム要素はキーボードでアクセス可能です。

### 5. タイトルの設定 (WCAG 2.4.2 ページタイトル)
- 各ページに適切なタイトルが設定されています。

### 6. 時間要素の適切な使用 (WCAG 1.3.1 情報及び関係性)
- 日付情報が適切に`<time>`要素でマークアップされており、スクリーンリーダーが認識できるようになっています。

## 改善が必要な項目

### 1. 画像の代替テキスト (WCAG 1.1.1 非テキストコンテンツ)
- RSS Feedアイコンの画像に適切な代替テキストが設定されていますが、ブログ記事内の画像や他のアイコンについても確認が必要です。

### 2. フォーム要素のラベル (WCAG 3.3.2 ラベル又は説明)
- 検索フォームの入力欄にプレースホルダー「記事を検索...」はありますが、明示的な`<label>`要素がないため、スクリーンリーダーユーザーに検索フォームと認識されない可能性があります。

### 3. カラーコントラスト (WCAG 1.4.3 コントラスト)
- カラーコントラストの自動チェックを実行した結果、コントラスト比の問題は検出されませんでした。ホームページとブログページの両方で、テキストと背景のコントラスト比はWCAG 2.2レベルAAの基準(4.5:1以上)を満たしています。

### 4. リンクテキストの明確さ (WCAG 2.4.4 リンクの目的)
- 「RSS Feed」のようなリンクは目的が明確ですが、いくつかのリンクテキスト(特にブログ記事一覧ページの短いリンク)は文脈から切り離すと目的が不明確になる可能性があります。

### 5. ページの言語 (WCAG 3.1.1 ページの言語)
- HTML要素に`lang`属性が設定されているか確認が必要です。日本語コンテンツには`lang="ja"`が必要です。

### 6. 見出しマークアップの問題 (WCAG 1.3.1 情報及び関係性)
- いくつかの見出しにマークダウン形式の記号(`##`など)が残っています。これは見た目だけの問題ですが、スクリーンリーダーがこれを読み上げる可能性があります。見出しテキストから`##`などの記号を削除するべきです。

## HTML構造の詳細分析

サイトの基本構造は以下のようになっています:

```
- banner (ヘッダー)
  - リンク(ホームへ)
  - 検索フォーム
  - ナビゲーションリスト

- main (メインコンテンツ)
  - 自己紹介文
  - セクション:ブログ記事
    - 見出し
    - 記事リスト
  - セクション:Cosenseの最近の投稿
    - 見出し
    - リスト
  - セクション:作ったもの・登壇資料
    - 見出し
    - 説明文

- contentinfo (フッター)
  - ナビゲーション
  - コピーライト情報
```

この構造は意味的に適切であり、スクリーンリーダーユーザーがサイトの内容を理解するのに役立ちます。

## 詳細な対応策

1. **検索フォームに明示的なラベルを追加**:
   ```html
   <label for="search">記事を検索:</label>
   <input id="search" type="text" placeholder="記事を検索...">
   ```

2. **全ての画像に適切な代替テキストを設定**:
   ```html
   <img src="icon.png" alt="アイコンの説明">
   ```

3. **HTML要素に言語属性を設定**:
   ```html
   <html lang="ja">
   ```

4. **リンクテキストを明確に**:
   コンテキストから切り離されても理解できるリンクテキストを使用する。

5. **カラーコントラスト比の確認**:
   テキストと背景のコントラスト比が4.5:1以上あることを確認する。
   
6. **見出しテキストのマークダウン記号を削除**:
   ```html
   <!-- 変更前 -->
   <h2>## ブログの記事</h2>
   
   <!-- 変更後 -->
   <h2>ブログの記事</h2>
   ```

## 対応の優先順位

1. 言語属性の追加(最優先)
2. フォーム要素のラベル追加
3. 見出しテキストの修正
4. 画像の代替テキスト確認
5. リンクテキストの改善
6. カラーコントラスト確認

## 参考リソース

- [W3C Web Content Accessibility Guidelines (WCAG) 2.2](https://www.w3.org/TR/WCAG22/)
- [WebAIM: Web Accessibility In Mind](https://webaim.org/)
- [日本語版 JIS X 8341-3:2016 アクセシビリティガイドライン](https://waic.jp/docs/jis2016/) 

そこそこ細かい所は検知してくれた。しかしスナップショットモードからは、コントラストのチェックは流石にできなかった。ということでPlaywright MCPで、コントラストチェックをできないのか聴いてみたら、Playwright内でaxe-coreを実行することでチェックできるとのことだった。

コントラストチェックをさせる

試しにサンプルのテストコードを書かせたら、以下のようなコードが出力された。

// カラーコントラスト分析用のPlaywrightスクリプト
const { chromium } = require('playwright');

async function runContrastTest() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  console.log('カラーコントラスト分析を開始します...');

  // サイトにアクセス
  await page.goto('http://localhost:4322/');

  // axe-coreをページに注入して実行
  const axeResults = await page.evaluate(async () => {
    // axe-coreをCDNから読み込む
    await new Promise(resolve => {
      const script = document.createElement('script');
      script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.0/axe.min.js';
      script.onload = resolve;
      document.head.appendChild(script);
    });

    // axe-coreを実行してアクセシビリティ評価
    return await window.axe.run();
  });

  // コントラスト関連の問題を抽出
  const contrastIssues = axeResults.violations.filter(v => v.id === 'color-contrast');

  console.log('カラーコントラスト問題の検出数:', contrastIssues.length);

  // 詳細情報を出力
  if (contrastIssues.length > 0) {
    console.log('検出されたカラーコントラスト問題:');
    contrastIssues.forEach((issue, index) => {
      console.log(`問題 ${index + 1}:`);
      console.log(`- 説明: ${issue.help}`);
      console.log(`- 影響度: ${issue.impact}`);
      console.log('- 該当要素:');

      issue.nodes.forEach((node, nodeIndex) => {
        console.log(`  要素 ${nodeIndex + 1}: ${node.html}`);
        console.log(`  問題箇所: ${node.failureSummary}`);
      });
    });
  } else {
    console.log('カラーコントラスト問題は検出されませんでした。');
  }

  // ブログページの確認も行う
  await page.goto('http://localhost:4322/blog');

  // axe-coreをページに注入して実行(ブログページ)
  const blogAxeResults = await page.evaluate(async () => {
    // axe-coreが既に読み込まれていることを確認
    if (typeof window.axe === 'undefined') {
      await new Promise(resolve => {
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.0/axe.min.js';
        script.onload = resolve;
        document.head.appendChild(script);
      });
    }

    // axe-coreを実行
    return await window.axe.run();
  });

  // ブログページのコントラスト関連の問題を抽出
  const blogContrastIssues = blogAxeResults.violations.filter(v => v.id === 'color-contrast');

  console.log('\nブログページのカラーコントラスト問題の検出数:', blogContrastIssues.length);

  // ブログページの詳細情報を出力
  if (blogContrastIssues.length > 0) {
    console.log('ブログページで検出されたカラーコントラスト問題:');
    blogContrastIssues.forEach((issue, index) => {
      console.log(`問題 ${index + 1}:`);
      console.log(`- 説明: ${issue.help}`);
      console.log(`- 影響度: ${issue.impact}`);
      console.log('- 該当要素:');

      issue.nodes.forEach((node, nodeIndex) => {
        console.log(`  要素 ${nodeIndex + 1}: ${node.html}`);
        console.log(`  問題箇所: ${node.failureSummary}`);
      });
    });
  } else {
    console.log('ブログページでカラーコントラスト問題は検出されませんでした。');
  }

  // 結果をオブジェクトとして返す(レポート追記用)
  const result = {
    homepage: {
      totalIssues: contrastIssues.length,
      details: contrastIssues,
    },
    blogpage: {
      totalIssues: blogContrastIssues.length,
      details: blogContrastIssues,
    },
  };

  await browser.close();
  return result;
}

// スクリプトを直接実行する場合
if (require.main === module) {
  runContrastTest()
    .then(result => {
      console.log('カラーコントラスト分析が完了しました。');
    })
    .catch(err => {
      console.error('分析中にエラーが発生しました:', err);
    });
}

module.exports = { runContrastTest };


なんとなく動きそうな気もするなーと思い、試しにそのまま実行すると動作はしてくれた。しかしaxe-coreを組み込む必要はあるのかなと疑問だった。そもそもChromeの拡張機能であるaxe-devtoolsを使うのと同じだと思ったからだ。

感想

かなり雑だったので、もう少し指示を明確にして設定をすればもう少し見れそうな気もした。時間があったらもう少し触ってみたい。