webapp-testing について
webapp-testingは、PlaywrightでWebアプリをテストするためのスキルです。開発者向けに、エンドツーエンドテスト、画面検証、アクセシビリティ監査、ネットワークのモック、モバイル表示の確認を案内します。
主な機能
- 01Playwrightによるエンドツーエンドテスト
- 02スクリーンショットとビジュアル回帰検証
- 03ブラウザーコンソールエラーの分析
- 04axe-coreによるアクセシビリティ監査
- 05ネットワークリクエストのモック
- 06モバイルビューポートのテスト
活用シーン
- 01重要なユーザーフローの検証
- 02変更時のページレイアウト確認
- 03ページのアクセシビリティ監査
- 04モバイルでのレスポンシブ動作のテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ pixel-process-ug/ superkit-agents --skill webapp-testingインストール後、Claude Code で/webapp-testingを入力して読み込まれたか確認します。
詳細
Web アプリのテスト
概要
Playwright を主なツールとして、包括的な Web アプリケーションのテストを行います。このスキルでは、視覚的な検証のためのスクリーンショットの撮影、ブラウザーのコンソールログの分析、ユーザー操作のシミュレーション、ビジュアルリグレッションテスト、axe-core によるアクセシビリティ監査、ネットワークリクエストのモック、モバイルのビューポートテストなど、エンドツーエンドのテストワークフローを扱います。
開始時に伝えること:「Playwright ベースの Web アプリケーションテストに webapp-testing スキルを使用します。」
フェーズ 1:テスト計画
目標: テスト対象を特定し、インフラストラクチャを準備します。
アクション
- テストする重要なユーザーフローを特定する
- テスト環境とビューポートを定義する
- テストフィクスチャとデータを準備する
- Playwright のプロジェクト設定を構成する
- ビジュアルベースラインのスクリーンショットを用意する
ユーザーフローの優先度判断表
| フローの種類 | 優先度 | テストの深さ |
|---|---|---|
| 認証(ログイン/ログアウト/登録) | 重大 | 正常系とエラー系の全パス |
| 中核的なビジネスワークフロー(購入、送信) | 重大 | 正常系、エラー系、エッジケースの全パス |
| ナビゲーションとルーティング | 高 | 主要なルートすべて |
| 検索とフィルタリング | 高 | 一般的なクエリと空の状態 |
| 設定とプロフィール | 中 | 正常系 |
| 管理者/バックオフィス | 中 | 主要な操作のみ |
停止 — 以下を満たすまでフェーズ 2 に進まないでください:
- 重要なユーザーフローを特定し、優先順位を付けている
- テスト環境とビューポートを定義している
- Playwright の設定が整っている
- テストデータの方針を定義している
フェーズ 2:テストの実装
目標: ページオブジェクトモデルとアクセシブルなロケーターを使ってテストを記述します。
アクション
- 主要なページのページオブジェクトモデルを作成する
- エンドツーエンドのテストシナリオを実装する
- ビジュアルリグレッション用のスナップショットを追加する
- アクセシビリティチェックを統合する
- 独立したテスト用にネットワークモックを設定する
Playwright の設定
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
ページオブジェクトモデル
class LoginPage {
constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel('Email');
readonly passwordInput = this.page.getByLabel('Password');
readonly submitButton = this.page.getByRole('button', { name: 'Sign in' });
readonly errorMessage = this.page.getByRole('alert');
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
ロケーター選択の判断表
| ロケーターの種類 | 優先度 | 使用する場面 |
|---|---|---|
getByRole | 第 1 選択肢 | ARIA ロール(ボタン、リンク、見出し)を持つ要素 |
getByLabel | 第 2 選択肢 | ラベル付きのフォームフィールド |
getByPlaceholder | 第 3 選択肢 | 表示ラベルのないフィールド |
getByText | 第 4 選択肢 | 操作対象ではない表示テキスト |
getByTestId | 最終手段 | アクセシブルなロケーターが使えない場合 |
| CSS セレクター/XPath | 使用しない | スタイルの変更で壊れるため |
停止 — 以下を満たすまでフェーズ 3 に進まないでください:
- 主要なページのページオブジェクトモデルが存在する
- テストではアクセシブルなロケーターのみを使用している
- ビジュアルベースラインを確立している
- アクセシビリティチェックを統合している
- 独立したテスト用にネットワークモックを設定している
フェーズ 3:CI への統合
目標: CI で信頼性が高く高速なテスト実行を設定します。
アクション
- ヘッドレスブラウザーでの実行を設定する
- スクリーンショットのアーティファクト収集を準備する
- リトライと不安定なテストの検出を設定する
- レポート機能を追加する(HTML レポート、JUnit XML)
- ビジュアル差分のレビュー手順を準備する
CI 設定チェックリスト
- CI でテストがヘッドレス実行される
- リトライが有効になっている(CI では 2 回リトライ)
- 失敗時にスクリーンショットと動画のアーティファクトが収集される
- CI 連携用の JUnit XML が出力される
- 手動レビュー用の HTML レポートが生成される
- マージ前にビジュアル差分のスナップショットをレビューする
停止 — 以下を満たせば CI への統合は完了です:
- CI パイプラインでテストが安定して実行される
- 失敗時にアーティファクトが収集される
- 不安定なテストを特定して修正している(スキップしていない)
スクリーンショット撮影のパターン
ページ全体
test('homepage renders correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
要素単位
test('navigation bar matches design', async ({ page }) => {
await page.goto('/');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('navbar.png');
});
動的コンテンツのマスキング
test('dashboard layout', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.locator('[data-testid="timestamp"]'),
page.locator('[data-testid="user-avatar"]'),
page.locator('.chart-container'),
],
animations: 'disabled',
});
});
ブラウザーログの分析
test('no console errors on page load', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
page.on('pageerror', error => {
consoleErrors.push(error.message);
});
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(consoleErrors).toEqual([]);
});
axe-core によるアクセシビリティテスト
import AxeBuilder from '@axe-core/playwright';
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('.third-party-widget')
.analyze();
expect(results.violations).toEqual([]);
});
ネットワークリクエストのモック
test('displays users from API', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
});
test('handles API errors gracefully', async ({ page }) => {
await page.route('**/api/users', route =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/users');
await expect(page.getByText('Something went wrong')).toBeVisible();
});
モバイルのビューポートテスト
test.describe('mobile responsive', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('hamburger menu works', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).not.toBeVisible();
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
});
テストの構成
tests/
e2e/
auth/
login.spec.ts
register.spec.ts
checkout/
cart.spec.ts
payment.spec.ts
fixtures/
test-data.ts
auth.setup.ts
pages/
login.page.ts
dashboard.page.ts
utils/
helpers.ts
アンチパターン/よくある間違い
| アンチパターン | 問題である理由 | 正しいアプローチ |
|---|---|---|
| CSS セレクターまたは XPath | スタイルの変更で動作しなくなります | アクセシブルなロケーター(ロール、ラベル、テキスト)を使用します |
page.waitForTimeout() | 根拠のない遅延が発生し、テストが不安定になります | expect().toBeVisible() などを使用します |
| サードパーティーコンポーネントの詳細なテスト | テスト対象のコードではありません | 内部実装ではなく、連携部分をテストします |
| テストデータのハードコード | 環境をまたぐと動作しなくなります | フィクスチャとファクトリーを使用します |
| 実行順序に依存するテスト | 壊れやすく、デバッグが困難です | 各テストは独立している必要があります |
| 不安定なテストを放置する | テストスイートへの信頼が損なわれます | 根本原因を修正するか、隔離します |
| 動的コンテンツをマスクしないスクリーンショット | 常に差異が生じ、常に失敗します | タイムスタンプ、アバター、グラフをマスクします |
| アクセシビリティチェックがない | 重要な品質ゲートが抜け落ちます | すべてのページで axe-core を使用します |
統合ポイント
| スキル | 関係 |
|---|---|
senior-frontend | フロントエンドコンポーネントは E2E テストでテストします |
testing-strategy | E2E テストはテストピラミッドの頂点です |
acceptance-testing | ユーザーフローテストは受け入れテストとして機能します |
performance-optimization | パフォーマンス予算は E2E テストで検証できます |
code-review | テストでアクセシブルなロケーターが使われているかをレビューします |
security-review | セキュリティヘッダーと認証フローを E2E テストで検証します |
品質チェックリスト
- 重要なユーザーフローをすべてカバーしている
- テストでアクセシブルなロケーター(ロール、ラベル、テキスト)を使用している
- テストを分離するためにネットワークをモックしている
- ビジュアルリグレッションのベースラインをレビューし、承認している
- すべてのページでアクセシビリティスキャンを実施している
- レスポンシブ機能について、モバイルビューポートでテストしている
-
waitForTimeoutを使用していない(適切なアサーションを使用する) - CI パイプラインでリトライを設定している
- 失敗時にスクリーンショットの成果物を収集している
- 不安定なテストを特定して修正している(スキップしていない)
スキルタイプ
FLEXIBLE — プロジェクトの重要なパスに合わせてテストの深さを調整します。ページオブジェクトモデルのパターンとアクセシブルなロケーターを強く推奨します。すべてのページでアクセシビリティチェックを必須とします。マージ前にビジュアルリグレッションのベースラインをレビューする必要があります。
コメント
Markdown 対応