失敗しないアクセシビリティテスト完全戦略:自動・手動・ユーザー・CI/CDで“壊れにくい品質”を育てる
概要サマリー(先に要点)
- 4層テスト戦略(自動テスト → 手動検証 → 支援技術検証 → ユーザーテスト)で抜け漏れを最小化。
- CI/CDに常設してリグレッション(回帰)を防止、デプロイ前に“赤”を止める。
- 仕様化の鍵は「達成基準→観察可能な振る舞い」への言い換え。誰が見ても同じ判定に。
- サンプルコード(Jest + jest-axe/Playwright + axe/Cypress + cypress-axe/pa11y-ci/Lighthouse CI)をそのまま導入。
- 手動チェック手順(キーボード、コントラスト、拡大・リフロー、動作削減、モバイル)と支援技術テスト(NVDA/VoiceOver/TalkBack)を標準化。
- レポートの書き方(What/Where/Why/How)と優先度付け基準(影響×頻度×回避不能性)。
- 誰が恩恵を受けるかを明確化し、組織に運用を根づかせるための方針・DoD(Doneの定義)テンプレート付き。
対象読者(具体):フロントエンドエンジニア、UI/UXデザイナー、QA/テストエンジニア、PM/Webディレクター、CS/サポート担当
アクセシビリティレベル:本記事は WCAG 2.1 AA 準拠を基準に、可能であれば WCAG 2.2 の追加項目(ターゲットサイズ・ドラッグ代替・フォーカス外観など)を段階導入する前提で構成しています。
1. はじめに:テストは“最終工程”ではなく“設計そのもの”です
アクセシビリティは、HTMLの意味(セマンティクス)・操作可能性・理解可能性が土台。ゆえにテストはバグ取りの最終工程ではなく、要件定義と設計の延長にあります。
- 早期発見のコスパ:デザイン・実装の初期にわかる問題(見出し階層、色依存、フォーカス欠落)は修正コストが小さい。
- 回帰の顕在化:新UIの追加や文言更新で、フォーカス順やコントラストが崩れることは日常茶飯事。CIで常時監視が必須ですわ。
- “誰が見ても同じ判定”:達成基準を「観察可能な振る舞い」に落とし込むと、チーム全員が同じ基準で品質を語れます。
2. 4層テスト戦略の全体像(重ねて守る)
- レイヤー1:自動テスト(静的・実行時)
- ルールに落とし込める問題(ラベルの欠落、roleの不整合、ARIA誤用、コントラストの多く など)を機械で一掃。
- レイヤー2:手動検証(動作・視覚)
- キーボード操作・フォーカス順・視認性・拡大など、人の感覚でしか判断できない部分を確認。
- レイヤー3:支援技術検証(SR・拡大鏡 等)
- NVDA/VoiceOver/TalkBackなど、実際の読み上げ・移動をざっくりでも毎リリースで触る。
- レイヤー4:ユーザーテスト(当事者/多様な利用者)
- 実利用の課題(専門用語、認知負荷、予測可能性の欠如)を観察で拾う。
すべて毎回は大変なので、デイリーは1・2、スプリント末に3、四半期に4のように頻度を決めると続けやすいです。
3. スコーピング:どこから始め、何を終わりとするか
- 優先ページ:トップ、検索・一覧、詳細、フォーム送信、アカウント、購入完了など主要ファネル。
- 代表端末:PC(Chrome/Edge + NVDA)、Mac(Safari + VoiceOver)、iOS(Safari + VoiceOver)、Android(Chrome + TalkBack)。
- 代表環境:縮小画面幅(320px想定)、フォント拡大(OS150%)、ダーク/ライト、動作削減オン。
- 終了条件(DoD):
- 自動検査は 重大(critical)違反0。
- 手動8項目(後述)を満たす。
- 主要シナリオ(検索→詳細→購入)がキーボードのみで完了。
- SRで見出し・ランドマーク・フォームが論理的に読み上げられる。
4. レイヤー1:自動テストを配管する(サンプルコード多数)
4.1 単体・コンポーネント:Jest + jest-axe
// __tests__/button.a11y.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import Button from '../Button';
expect.extend(toHaveNoViolations);
test('Button はa11y違反がない', async () => {
const { container } = render(<Button>送信</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
4.2 E2E:Playwright + @axe-core/playwright
// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('トップページのa11y検査(wcag2a/aa)', async ({ page }) => {
await page.goto('http://localhost:3000/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a','wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
4.3 ブラウザ操作テスト:Cypress + cypress-axe
// cypress/e2e/a11y.cy.js
import 'cypress-axe';
it('フォームがa11y違反なし', () => {
cy.visit('/contact');
cy.injectAxe();
cy.checkA11y(null, {
runOnly: { type: 'tag', values: ['wcag2a','wcag2aa'] }
});
});
4.4 サイト全体のクロール:pa11y-ci
// .pa11yci
{
"defaults": { "standard": "WCAG2AA", "timeout": 30000, "concurrency": 4 },
"urls": [
"http://localhost:3000/",
"http://localhost:3000/search",
"http://localhost:3000/contact"
]
}
4.5 品質ゲート:Lighthouse CI(アクセシビリティカテゴリ)
// .lighthouserc.js
module.exports = {
ci: {
collect: { staticDistDir: "./dist" },
assert: { assertions: { "categories:accessibility": ["error", { minScore: 0.9 }] } },
upload: { target: "temporary-public-storage" }
}
};
4.6 CI/CD(例:GitHub Actions)
# .github/workflows/a11y.yml
name: a11y-ci
on: [push, pull_request]
jobs:
axe-playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run build && npm run start & npx wait-on http://localhost:3000
- run: npx playwright install --with-deps
- run: npx playwright test -g "a11y検査"
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci && npm run build
- run: npx lhci autorun
注意:自動検査は万能ではありません。コントラスト・ラベル・構造の多くは拾えますが、意味・理解・予測可能性は人が確かめる必要があります。
5. レイヤー2:手動検証の標準手順(“8つの柱”)
-
キーボード操作(2.1.1/2.4.3/2.4.7)
- Tabで論理順に巡回できる/
Shift+Tab
で逆順でも破綻がない。 - スキップリンクが最初のTabで現れ、
main
へ移動。 - ドロップダウン・モーダル:Enter/Space開閉、Escで閉じ、トリガに復帰。
- Tabで論理順に巡回できる/
-
フォーカスの可視化(2.4.7)
- すべてのフォーカス可能要素に明確なインジケーター(太さ・色コントラスト)。
:focus-visible
でマウスと区別し、見失わない表示に。
-
コントラスト(1.4.3/1.4.11)
- 本文4.5:1・大きな文字3:1、アイコン/境界線など非テキスト3:1。
- エラーメッセージやリンクも基準を満たす。
-
画像・代替テキスト(1.1.1)
- 情報画像に要約alt、装飾は
alt=""
(またはCSS背景)。 - アイコンボタンは可視ラベルまたは
aria-label
で意味が伝わる。
- 情報画像に要約alt、装飾は
-
見出し・ランドマーク(1.3.1/2.4.1)
h1
は1つ。階層はh2→h3…
で論理的。header/nav/main/aside/footer
を適切配置。複数あるnav
はラベル付き。
-
拡大・リフロー(1.4.10/1.4.12)
- 幅320px相当で横スクロール強要なし(地図等の例外除く)。
- 行間1.5、段落2em、文字間0.12em、単語間0.16emでも欠落・重なりなし。
-
動き・点滅(2.3/動作削減)
prefers-reduced-motion: reduce
で大きな動きが弱まる/停止。- 点滅(3回/秒以上)は避ける。
-
モバイル(ターゲットサイズ・向き・代替操作)
- タッチターゲットは44〜48px目安、近接しすぎない。
- 縦横の強制なし、複雑ジェスチャは一本指代替を用意。
6. レイヤー3:支援技術(スクリーンリーダー等)検証のコア観点
- 読み上げの“地図”
- 見出しの一覧でページ構造が追えるか。
- ランドマークで主要領域を移動できるか。
- フォーム
- ラベル・ヒント・エラーが**
aria-describedby
**で結びつく。 - エラー発生時は**
role="alert"
** で即時通知。
- ラベル・ヒント・エラーが**
- ダイアログ
- 開いたらタイトルにフォーカス、
aria-modal="true"
、フォーカストラップ、Esc閉じ、トリガ復帰。
- 開いたらタイトルにフォーカス、
- ライブ更新
role="status"
(控えめ)/role="alert"
(緊急)で過不足ない通知。
最小セット:NVDA(Windows)・VoiceOver(macOS/iOS)・TalkBack(Android)で各5分だけでも毎リリース触ると、問題の早期検知力が段違いですわ。
7. レイヤー4:ユーザーテスト(当事者・多様な利用者)の設計
7.1 テスト設計のステップ
- 目的と仮説:例)「キーボードのみでチェックアウト完了まで迷わない」
- タスク設計:例)商品検索 → カート → 住所入力 → 注文確定
- 参加条件:支援技術利用の有無、モバイル/PC、色覚特性などを幅広く。
- 環境準備:SRのログ取り、画面・音声の録画、同意の取得。
- 観察指標:完了率、所要時間、エラー数、発話プロトコルの気づき。
7.2 セッション進行の雛形
- 導入:「普段どおりにご利用ください。サイトの評価であって、あなたの評価ではありません」
- 実施:観察者は口を挟まない。詰まったら「いま何が起きていますか?」程度の促し。
- 振り返り:困りごとを具体例で聞く(例:「〇〇のボタンは見つけやすかったですか?」)。
ユーザーテストはWCAGの適否が目的ではなく、理解可能性・予測可能性・学習コストを見ます。自動や手動では出ない“実用の壁”を発見できます。
8. レポートの書き方:誰が読んでも同じ行動に至る
ひとつの問題 = ひとつのチケットで、以下を必須にします。
- What(何が問題?):観察できる事実(例:「Tabでヘッダー内を循環して本文へ到達できない」)。
- Where(どこで?):URL/画面名/コンポーネント名(再現パス)。
- Why(なぜ問題?):ユーザー影響(例:「キーボード利用者が主要内容へ到達できず離脱」)。
- Which(基準):該当するWCAG(例:2.4.1/2.1.1)。
- How(どう直す?):修正案(例:スキップリンク追加、
main#content
へフォーカス移動)。 - 証跡:スクリーンショット/動画/SRログ/HTML断片。
優先度付け(P1〜P4)の目安
- P1(今すぐ):致命(完了不能・情報欠落・発作リスク)
- P2(次リリース):主要フロー阻害・大多数に影響
- P3(計画的に):回避策あり・軽微だが累積でUX低下
- P4(観察):改善提案・将来の負債候補
9. 受け入れ基準(DoD)テンプレート
- [ ] 自動検査(axe/pa11y/LHCI)で重大違反0。
- [ ] 手動8項目を満たす(§5)。
- [ ] 主要シナリオをキーボードのみで完遂。
- [ ] SRで見出し・ランドマーク・フォームが論理的に読まれる。
- [ ] モバイルでターゲットサイズ・拡大・向き・代替操作が機能。
- [ ] 変更箇所の回帰テスト(スナップショット/E2E)に合格。
- [ ] リリースノートにアクセシビリティ影響(改善・既知の制約)を記載。
10. サンプル:デザインレビュー用 “即席チェックカード”
10.1 レイアウト・情報構造
- ☐
h1
は1つ。h2→h3
の階層が論理的。 - ☐
nav/main/footer
があり、複数のnav
はラベル付き。 - ☐ DOM順=視覚順。CSSで順序を入れ替えていない。
10.2 コントラスト・色
- ☐ テキスト4.5:1/大きい文字3:1。
- ☐ リンク・アイコン・フォーカスリングなど非テキスト3:1。
- ☐ 色のみで伝えない(アイコン・ラベル・形状の併用)。
10.3 フォーム
- ☐ すべての入力に可視ラベル。
- ☐ ヒント・エラーは
aria-describedby
で関連付け、role="alert"
で通知。 - ☐ 必須表示は色+テキスト。
10.4 インタラクション
- ☐ ドロップダウン・タブ・モーダルは期待されるキー操作が機能。
- ☐ 開閉状態は
aria-expanded
等で状態が伝わる。 - ☐ 初期フォーカス・復帰先が自然。
10.5 モバイル
- ☐ タッチターゲット44〜48px、近接しすぎない。
- ☐ 拡大許可・320px幅でも崩れない。
- ☐ 縦横どちらも操作可能。
11. 組織運用:デザインシステムとCIに“恒常化”させる
- デザインシステムに、コンポーネントごとの**a11y仕様(名前・役割・値・キー操作・コントラスト)**を明文化。
- Storybookでアクセシビリティアドオンを有効化し、単位テスト+目視の“二刀流”。
- CIの失敗理由をSlack通知→担当自動アサイン。
- アクセシビリティ負債台帳を作り、P1/P2はスプリント中、P3/P4は四半期で計画返済。
- 教育:新メンバー向けに「15分の手動ルーティン」を動画化。
12. 誰に、どう効くの?(具体的インパクト)
-
フロントエンドエンジニア:
- 事前に“落とし穴”を自動で塞げるのでレビューが本質的に。
- 回帰に強くなり、土壇場の火消しが減る。
-
UI/UXデザイナー:
- コントラスト・ラベル・状態設計がデザインシステムに固定化。
- 検証観点が共有化され、議論が事実ベースに。
-
QA/テストエンジニア:
- 手動8項目とSR観点でテスト設計がテンプレ化。
- 重大度の判定が揺れない。
-
PM/Webディレクター:
- DoDがクリアでリリース判定が明快。
- 訴訟・炎上・解約などリスクの早期遮断。
-
CS/サポート:
- 問い合わせの再現性と回答の品質が上がる。
- 既知課題をナレッジ化しやすい。
-
利用者(支援技術ユーザー・高齢者・一時的制約のある人):
- 迷い・疲労・誤操作が減り、平等で安定した体験を得られる。
13. 参考テンプレート:アクセシビリティ方針(抜粋・社内外公開用)
当社アクセシビリティ方針(抜粋)
- 目標:すべての新規ページと主要機能で WCAG 2.1 AA 準拠。
- 手段:デザインレビューにa11yチェックカード、CIにaxe/LHCIを導入。
- テスト:各リリースで手動8項目とNVDA/VoiceOverの簡易検証を実施。
- 改善:問い合わせ・ユーザーテスト結果を四半期ごとに負債台帳へ登録し、優先度に基づき返済。
- 連絡先:アクセシビリティ窓口(メール/フォーム/電話・テキスト中継対応)。
- 免責・既知の制約:一部の過去PDF/外部ウィジェットは段階的に改善予定。
14. サンプル:Playwright E2Eに“キーボード流儀”を組み込む
import { test, expect } from '@playwright/test';
test('キーボードだけで検索→詳細→購入確認に到達できる', async ({ page }) => {
await page.goto('http://localhost:3000/');
// スキップリンク
await page.keyboard.press('Tab');
await expect(page.locator('a.skip')).toBeVisible();
await page.keyboard.press('Enter');
// 検索フォーム入力
await page.keyboard.type('キーボード対応');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// 結果一覧 → 最初の詳細へ
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// モーダルが開く → タイトルにフォーカス
await expect(page.locator('[role="dialog"] h2')).toBeFocused();
// Escで閉じ→トリガに復帰
await page.keyboard.press('Escape');
await expect(page.locator('a[aria-controls="modal"]')).toBeFocused();
});
自動テストに**“キーボードの旅路”を入れると、UI変更での破綻が即座に検知**できます。
15. よくある質問(FAQ)
Q1:自動検査を0違反にしても、手動で指摘が出るのはなぜ?
A:自動は「機械が判定できる事実」しか見ません。意味・予測可能性・理解容易性は人の判断が必要です。
Q2:SR検証は毎回どこまで?
A:毎リリースは見出し・ランドマーク・主要フォームの5分スモークを。大規模改修時に詳細検証を。
Q3:どのブラウザ/OSを優先?
A:トラフィックと支援技術の利用状況で決めます。最低限、Windows+NVDA、iOS+VoiceOver、Android+TalkBackをカバー。
16. アクセシビリティレベルの評価(本記事の到達点)
- 準拠目標:WCAG 2.1 AA
- 自動検査と手動8項目で 1.1.1 / 1.3.1 / 1.4.3 / 1.4.11 / 2.1.1 / 2.4.1 / 2.4.3 / 2.4.7 / 3.3.x / 4.1.2 に対応。
- 発展的対応(推奨):WCAG 2.2 AA の一部(2.5.7 Dragging Movements、2.5.8 Target Size (Minimum)、2.4.11 Focus Appearance)を運用へ段階導入。
- 読みやすさ配慮:本文は短文中心・見出しで階層化・専門用語に補足を添え、学習コストの低い表現を意識しています。
17. まとめ:品質は“仕組み”で守る。情熱は“観察”で磨く。
- 4層戦略(自動→手動→支援技術→ユーザー)で、抜け漏れを塞ぐ。
- CI/CD常設で回帰を防ぎ、赤はデプロイ前に止める。
- 観察可能な振る舞いに落とし込んだチェックリストで、判定の一貫性を確保。
- レポートの型(What/Where/Why/How)で、直す人に迷いを残さない。
- 負債台帳とDoDで、継続改善をプロセスに固定。
小さな“確認の習慣”が、大きな“安心の体験”を生みます。あなたのプロダクトが、誰にとっても迷いなく使える存在になるよう、わたしも心を込めて応援していますね。