Help us improve
Share bugs, ideas, or general feedback.
From reviw-plugin
Tests local web applications using Playwright: verifies frontend functionality, debugs UI behavior, captures screenshots, views logs. Mandatory before declaring implementation complete.
npx claudepluginhub kazuph/reviw --plugin reviw-pluginHow this skill is triggered — by the user, by Claude, or both
Slash command
/reviw-plugin:webapp-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
To test local web applications, write TypeScript E2E tests using **Playwright Test** (`@playwright/test`).
Automates browser tasks and E2E testing with Playwright: auto-detects dev servers, generates scripts for pages, forms, screenshots, responsive design, UX validation, login flows, cross-browser checks in TypeScript/JavaScript/Python projects.
Automates browser testing for web apps using Playwright MCP: navigate pages, click/fill elements, take screenshots, verify UI/console logs, debug frontend issues, validate responsive design.
Guides Playwright end-to-end testing for web apps with cross-browser support (Chromium, Firefox, WebKit), visual regression, API testing, and mobile emulation. Use for E2E tests and UI automation workflows.
Share bugs, ideas, or general feedback.
To test local web applications, write TypeScript E2E tests using Playwright Test (@playwright/test).
CRITICAL: E2E Test File Placement
tests/e2e/ or e2e/ directory at the project root.artifacts/ - that's for evidence only (screenshots, videos)User task → Is it static HTML?
├─ Yes → Read HTML file directly to identify selectors
│ ├─ Success → Write Playwright test using selectors
│ └─ Fails/Incomplete → Treat as dynamic (below)
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Use webServer config in playwright.config.ts
│ to auto-start the dev server
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
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',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test('should login successfully with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('invalid@example.com');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
});
});
# Run all E2E tests
npx playwright test
# Run specific test file
npx playwright test tests/e2e/login.spec.ts
# Run with UI mode (interactive debugging)
npx playwright test --ui
# Run headed (visible browser)
npx playwright test --headed
# Generate test code interactively
npx playwright codegen http://localhost:3000
When you don't know the page structure:
import { test, expect } from '@playwright/test';
test('discover and interact with page elements', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 1. Take screenshot for reconnaissance
await page.screenshot({ path: '/tmp/inspect.png', fullPage: true });
// 2. Log all buttons for analysis
const buttons = await page.getByRole('button').all();
for (const button of buttons) {
console.log('Button:', await button.textContent());
}
// 3. Get page content for selector discovery
const content = await page.content();
console.log(content);
});
When collecting evidence (screenshots/videos) for PR reviews, use this pattern:
# Run tests with evidence collection
FEATURE=${FEATURE:-feature}
mkdir -p .artifacts/$FEATURE/{images,videos}
npx playwright test tests/e2e/your-feature.spec.ts \
--headed \
--output=.artifacts/$FEATURE \
--trace=retain-on-failure \
--reporter=line
import { test, expect } from '@playwright/test';
test.describe('Feature Demo', () => {
test('demonstrate feature workflow', async ({ page }, testInfo) => {
const feature = process.env.FEATURE || 'feature';
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
await page.goto('/feature');
await page.waitForLoadState('networkidle');
// Capture before state
await page.screenshot({
path: `.artifacts/${feature}/images/${timestamp}-before.png`,
fullPage: true
});
// Perform actions
await page.getByRole('button', { name: 'Enable Feature' }).click();
await expect(page.getByText('Feature Enabled')).toBeVisible();
// Capture after state
await page.screenshot({
path: `.artifacts/${feature}/images/${timestamp}-after.png`,
fullPage: true
});
});
});
For quick checks without writing a full test file (use TypeScript via tsx):
npx tsx -e "
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.env.BASE_URL || 'http://localhost:3000', { waitUntil: 'networkidle' });
await page.screenshot({ path: '/tmp/webapp.png', fullPage: true });
await browser.close();
console.log('saved: /tmp/webapp.png');
"
E2Eテストとは別に、対話的にブラウザを操作するためのCLIツールが3つある。 Dogfooding、手動検証、デバッグに使う。
| ツール | パッケージ名 | 実行方法 |
|---|---|---|
| browser-use CLI v2 | browser-use (PyPI) | uvx browser-use <command> |
| agent-browser | agent-browser (npm) | npx agent-browser@latest <command> |
| Playwright CLI | @playwright/mcp (npm) | npx @playwright/mcp@latest (MCP server) |
注意: @anthropic-ai/claude-code-playwright は存在しない。Playwright公式は @playwright/mcp と @playwright/cli。
# 1. browser-use CLI v2(Python / uvx経由)
uv tool install browser-use
# 確認
uvx browser-use --help
# 2. agent-browser(Rust / npx経由)
npx agent-browser@latest --version
# 初回はnpxが自動インストール
# 3. Playwright CLI(Node.js / npx経由)
npx playwright --version
# ブラウザ未インストールなら:
npx playwright install chromium
# 3a. Playwright MCP(Claude Code等のAIツールから使う場合)
npx @playwright/mcp@latest
| ツール | 特徴 | 向いている場面 |
|---|---|---|
| browser-use v2 | Daemon型で高速(50ms/コマンド)。DOMを4段階圧縮して2-5KBに | サクッと確認したい時。速度重視 |
| agent-browser | Rust製。CDPのAccessibility API直接利用。cursor:pointer要素も検出 | 深く探りたい時。a11y問題の発見に強い |
| Playwright CLI | MCP対応。差分snapshot(2回目以降は変更分だけ) | トークン節約したい時。AI連携に最適 |
| トークン | 速度 | 発見の深さ | |
|---|---|---|---|
| Playwright CLI | 36K (1位) | 1,129秒 (2位) | 基本確認 |
| browser-use v2 | 45K (2位) | 264秒 (1位) | 中程度 |
| agent-browser | 53K (3位) | 3,494秒 (3位) | 最も深い(4件発見) |
uvx browser-use open http://localhost:3000 # ページを開く
uvx browser-use state # DOM状態をテキストで取得(トークン節約)
uvx browser-use click "button#submit" # クリック
uvx browser-use input "input#name" "テスト" # テキスト入力(typeではなくinput)
uvx browser-use screenshot /tmp/shot.png # スクリーンショット
uvx browser-use close # ブラウザを閉じる
npx agent-browser@latest open http://localhost:3000
npx agent-browser@latest snapshot # アクセシビリティツリー(トークン節約)
npx agent-browser@latest click @e5 # ref指定でクリック
npx agent-browser@latest fill @e3 "テスト" # ref指定で入力
npx agent-browser@latest screenshot /tmp/shot.png
npx agent-browser@latest close
npx @playwright/cli@latest open http://localhost:3000
npx @playwright/cli@latest snapshot # YAML形式アクセシビリティツリー(差分対応)
npx @playwright/cli@latest click --ref=e5 # ref指定でクリック
npx @playwright/cli@latest fill --ref=e3 "テスト"
npx @playwright/cli@latest screenshot # スクリーンショット
npx @playwright/cli@latest close
state / snapshot のテキスト出力で状態把握するstateはviewport内の要素だけ返すのでコンパクトsnapshotはインタラクティブ要素にrefを付与するので、意味的に密度が高い❌ Don't inspect the DOM before waiting for networkidle on dynamic apps
✅ Do wait for page.waitForLoadState('networkidle') before inspection
❌ Don't place E2E test files in .artifacts/
✅ Do place E2E tests in tests/e2e/ as permanent project assets
❌ Don't write tests in Python
✅ Do write tests in TypeScript using @playwright/test
❌ Don't use Vitest for E2E tests ✅ Do use Playwright Test for E2E, Vitest for unit/integration tests
tests/e2e/ - Permanent project assets, not disposablegetByRole, getByLabel, getByText over CSSwaitForLoadState, waitForSelector, expect().toBeVisible()webServer config - Auto-start dev server in playwright.config.tsproject/
├── playwright.config.ts # Playwright configuration
├── tests/
│ └── e2e/ # E2E tests (permanent)
│ ├── login.spec.ts
│ ├── checkout.spec.ts
│ └── settings.spec.ts
├── .artifacts/ # Evidence only (temporary)
│ └── <feature>/
│ ├── images/ # Screenshots
│ ├── videos/ # Recorded videos
│ └── REPORT.md # Review report
└── test-results/ # Playwright auto-generated (gitignored)
Key distinction:
tests/e2e/ = Permanent E2E test code (committed to repo).artifacts/ = Temporary evidence for PR review (gitignored or LFS)test-results/ = Playwright's auto-generated output (gitignored)test('collect console logs', async ({ page }) => {
const consoleLogs: string[] = [];
const errors: string[] = [];
page.on('console', msg => consoleLogs.push(`${msg.type()}: ${msg.text()}`));
page.on('pageerror', err => errors.push(err.message));
page.on('requestfailed', req => errors.push(`Request failed: ${req.url()}`));
await page.goto('/');
await page.waitForLoadState('networkidle');
console.log('Console logs:', consoleLogs);
if (errors.length > 0) {
console.error('Errors:', errors);
}
});
| Test Type | Tool | Reason |
|---|---|---|
| Unit tests | Vitest | Fast, no browser needed |
| Integration tests | Vitest | Fast, mock external deps |
| Component tests | Vitest + browser mode | or Storybook |
| E2E tests | Playwright Test | Full browser, real flows |
Do NOT use Vitest for E2E tests. Playwright Test has:
codegen)