AI開発プラットフォーム比較2025 - Langfuse・Helicone・Phala Cloudの完全ガイド
最新のAI開発プラットフォームLangfuse、Helicone、Phala Cloudを徹底比較。LLM監視、コスト管理、パフォーマンス最適化の観点から、プロジェクトに最適なプラットフォーム選択のガイドを提供します。
Vitest Browser ModeとPlaywrightを組み合わせた最新のコンポーネントテスト手法を徹底解説。実際のブラウザ環境でのテスト実行、パフォーマンス最適化、CI/CD統合、トラブルシューティングまで、現場で使える実践的なノウハウを紹介します。
2025 年のフロントエンド開発において、コンポーネントテストの品質が製品の成功を左右する時代になりました。JSDOM や Happy DOM では再現できない複雑なユーザーインタラクション、ブラウザ固有の API、視覚的な状態変化。これらを正確にテストするために、Vitest Browser Mode × Playwrightの組み合わせが新たな標準として注目されています。
本記事では、実際のプロジェクトで直面する課題を解決しながら、Browser Mode の真の力を引き出す方法を徹底的に解説します。
テストの課題 | 従来の手法の限界 | Browser Modeでの解決 |
---|---|---|
複雑なユーザーインタラクション | イベントのシミュレーションのみ | 実際のブラウザイベントを発火 |
ブラウザAPI依存 | モックが必要 | ネイティブAPIをそのまま利用 |
CSS/レイアウト | テスト不可能 | 実際のレンダリング結果を検証 |
Web Components | Shadow DOM非対応 | 完全サポート |
視覚的回帰 | 別ツールが必要 | スクリーンショット機能内蔵 |
パフォーマンス | 測定困難 | 実環境での計測が可能 |
実験的機能として初登場
並列実行とCDP対応
React/Vue/Solidの公式パッケージ提供
大手企業での採用事例増加
コンポーネントテストの新標準へ
チャートを読み込み中...
✅ ユーザーインタラクションが重要なコンポーネント ✅ ブラウザ api に依存する機能(File API、Clipboard api 等) ✅ css アニメーションやトランジション ✅ レスポンシブデザインのテスト ✅ web Components やカスタムエレメント ❌ 純粋なロジックのテスト(通常の Vitest で十分) ❌ シンプルな表示のみのコンポーネント
# Vitest と Browser Mode関連パッケージ
npm install -D vitest @vitest/browser
# Playwright provider
npm install -D playwright @vitest/browser-driver-playwright
# フレームワーク別パッケージ(必要に応じて)
npm install -D @vitest/browser-react # React
npm install -D @vitest/browser-vue # Vue
npm install -D @vitest/browser-solid # Solid
# Vitest と Browser Mode関連パッケージ
pnpm add -D vitest @vitest/browser
# Playwright provider
pnpm add -D playwright @vitest/browser-driver-playwright
# フレームワーク別パッケージ(必要に応じて)
pnpm add -D @vitest/browser-react # React
pnpm add -D @vitest/browser-vue # Vue
pnpm add -D @vitest/browser-solid # Solid
# Vitest と Browser Mode関連パッケージ
yarn add -D vitest @vitest/browser
# Playwright provider
yarn add -D playwright @vitest/browser-driver-playwright
# フレームワーク別パッケージ(必要に応じて)
yarn add -D @vitest/browser-react # React
yarn add -D @vitest/browser-vue # Vue
yarn add -D @vitest/browser-solid # Solid
# Vitest と Browser Mode関連パッケージ
bun add -d vitest @vitest/browser
# Playwright provider
bun add -d playwright @vitest/browser-driver-playwright
# フレームワーク別パッケージ(必要に応じて)
bun add -d @vitest/browser-react # React
bun add -d @vitest/browser-vue # Vue
bun add -d @vitest/browser-solid # Solid
// tsconfig.json
{
"compilerOptions": {
"types": [
"node",
"vite/client",
"@vitest/browser/matchers"
]
},
"include": [
"src/**/*",
"vitest.workspace.ts"
]
}
{
"scripts": {
"test": "vitest",
"test:unit": "vitest --project=unit",
"test:browser": "vitest --project=browser",
"test:browser:ui": "vitest --project=browser --ui",
"test:browser:debug": "vitest --project=browser --no-headless"
}
}
// Button.browser.test.tsx
import { expect, test } from 'vitest'
import { page, userEvent } from '@vitest/browser/context'
import { render } from '@vitest/browser-react'
import { Button } from './Button'
test('ボタンクリックイベントが正しく発火する', async () => {
// Arrange
const handleClick = vi.fn()
const { getByRole } = render(
<Button onClick={handleClick}>Click me</Button>
)
// Act
const button = getByRole('button')
await userEvent.click(button)
// Assert
expect(handleClick).toHaveBeenCalledTimes(1)
})
test('フォーカス状態のスタイルが適用される', async () => {
// Arrange
const { getByRole } = render(<Button>Focus me</Button>)
const button = getByRole('button')
// Act
await userEvent.tab()
// Assert
// 実際のブラウザでComputedStyleを検証
const styles = window.getComputedStyle(button)
expect(styles.outline).toBe('2px solid blue')
})
// DragAndDrop.browser.test.tsx
test('ドラッグ&ドロップが正しく動作する', async () => {
const { getByTestId } = render(<DragAndDropList />)
const item1 = getByTestId('item-1')
const item2 = getByTestId('item-2')
// 実際のドラッグ&ドロップ操作
await userEvent.dragAndDrop(item1, item2)
// 順序が入れ替わったことを確認
const items = await page.locator('[data-testid^="item-"]').all()
expect(await items[0].textContent()).toBe('Item 2')
expect(await items[1].textContent()).toBe('Item 1')
})
// FileUpload.browser.test.tsx
test('ファイルアップロードが正しく処理される', async () => {
const { getByLabelText } = render(<FileUpload />)
// テスト用ファイルを作成
const file = new File(['hello'], 'hello.txt', {
type: 'text/plain'
})
const input = getByLabelText('ファイルを選択')
// ネイティブのファイル選択をシミュレート
await userEvent.upload(input, file)
// ファイル情報が表示されることを確認
await expect.element(page.getByText('hello.txt')).toBeVisible()
await expect.element(page.getByText('10 bytes')).toBeVisible()
})
// Clipboard.browser.test.tsx
test('クリップボードのコピー&ペーストが動作する', async () => {
const { getByRole, getByLabelText } = render(<ClipboardDemo />)
// コピー元のテキスト
const source = getByTestId('source')
await userEvent.fill(source, 'Hello, Browser Mode!')
// コピーボタンをクリック
await userEvent.click(getByRole('button', { name: 'コピー' }))
// ペースト先にフォーカス
const target = getByLabelText('ペースト先')
await userEvent.click(target)
// Ctrl+V でペースト
await userEvent.keyboard('Control+V')
// ペーストされたことを確認
expect(await target.inputValue()).toBe('Hello, Browser Mode!')
})
// VisualRegression.browser.test.tsx
import { expect, test } from 'vitest'
import { page } from '@vitest/browser/context'
import { render } from '@vitest/browser-react'
import { Card } from './Card'
test('カードコンポーネントの見た目が変わっていない', async () => {
// コンポーネントをレンダリング
render(
<Card
title="テストカード"
description="これはテスト用のカードです"
image="/test-image.jpg"
/>
)
// スクリーンショットを撮影して比較
await expect.element(page).toMatchScreenshot('card-component.png', {
maxDiffPixels: 100,
threshold: 0.2
})
})
test('ホバー状態のスタイルが正しい', async () => {
const { getByTestId } = render(<Card hoverable />)
const card = getByTestId('card')
// ホバー前のスクリーンショット
await expect.element(card).toMatchScreenshot('card-normal.png')
// ホバー状態
await userEvent.hover(card)
await page.waitForTimeout(300) // アニメーション完了待ち
// ホバー後のスクリーンショット
await expect.element(card).toMatchScreenshot('card-hover.png')
})
// useIntersectionObserver.browser.test.tsx
import { renderHook } from '@vitest/browser-react'
import { useIntersectionObserver } from './useIntersectionObserver'
test('要素が画面内に入った時にコールバックが呼ばれる', async () => {
const callback = vi.fn()
// カスタムフックをレンダリング
const { result } = renderHook(() =>
useIntersectionObserver(callback)
)
// 要素を作成してrefに設定
const element = document.createElement('div')
element.style.height = '100px'
document.body.appendChild(element)
// refに要素を設定
act(() => {
result.current.ref.current = element
})
// スクロールして要素を画面内に
element.scrollIntoView()
// IntersectionObserverのコールバックを待つ
await waitFor(() => {
expect(callback).toHaveBeenCalledWith(true)
})
})
<!-- SearchInput.browser.test.ts -->
<script setup lang="ts">
import { expect, test } from 'vitest'
import { page, userEvent } from '@vitest/browser/context'
import { render } from '@vitest/browser-vue'
import SearchInput from './SearchInput.vue'
test('検索入力のデバウンスが正しく動作する', async () => {
const onSearch = vi.fn()
const { getByPlaceholderText } = render(SearchInput, {
props: {
debounceMs: 300,
onSearch
}
})
const input = getByPlaceholderText('検索...')
// 高速に文字を入力
await userEvent.type(input, 'Vue')
// デバウンス時間前はコールバックが呼ばれない
expect(onSearch).not.toHaveBeenCalled()
// デバウンス時間経過後
await page.waitForTimeout(350)
// 最終的な値で1回だけ呼ばれる
expect(onSearch).toHaveBeenCalledTimes(1)
expect(onSearch).toHaveBeenCalledWith('Vue')
})
</script>
// vitest.config.ts - 最適化設定
export default defineConfig({
test: {
browser: {
provider: 'playwright',
providerOptions: {
// 並列実行の設定
launch: {
args: ['--disable-blink-features=AutomationControlled']
}
}
},
// テストの並列実行
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 4,
minThreads: 1
}
},
// タイムアウト設定
testTimeout: 30000,
hookTimeout: 10000
}
})
# .github/workflows/test.yml
name: Component Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright
run: pnpm exec playwright install --with-deps chromium
- name: Run Browser Tests
run: pnpm test:browser
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-results/
**/*.png
# .gitlab-ci.yml
browser-tests:
image: mcr.microsoft.com/playwright:v1.42.0-focal
stage: test
script:
- npm ci
- npm run test:browser
artifacts:
when: always
paths:
- test-results/
reports:
junit: test-results/junit.xml
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- ~/.cache/ms-playwright/
# .circleci/config.yml
version: 2.1
orbs:
browser-tools: circleci/browser-tools@1.4
jobs:
browser-test:
docker:
- image: cimg/node:20.0-browsers
steps:
- checkout
- browser-tools/install-chrome
- browser-tools/install-chromedriver
- restore_cache:
keys:
- v1-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- run: npx playwright install
- run: npm run test:browser
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
1. “Cannot find module ‘@vitest/browser/context’”
# 解決方法
npm install -D @vitest/browser
2. “Timeout waiting for selector”
// タイムアウトを延長
await expect.element(selector).toBeVisible({ timeout: 10000 })
3. “Browser closed unexpectedly”
// ブラウザの起動オプションを調整
providerOptions: {
launch: {
args: ['--no-sandbox', '--disable-setuid-sandbox']
}
}
// デバッグモードでテストを実行
test('デバッグが必要なテスト', async () => {
// ブレークポイントを設定
debugger
// ブラウザの開発者ツールでデバッグ
await page.pause()
// スクリーンショットを撮影
await page.screenshot({ path: 'debug.png' })
// HTMLを出力
console.log(await page.content())
})
test('パフォーマンステスト', async () => {
// パフォーマンス計測開始
await page.evaluate(() => {
performance.mark('test-start')
})
// テスト対象の操作
render(<HeavyComponent />)
// パフォーマンス計測終了
const metrics = await page.evaluate(() => {
performance.mark('test-end')
performance.measure('test-duration', 'test-start', 'test-end')
const measure = performance.getEntriesByName('test-duration')[0]
return {
duration: measure.duration,
memory: (performance as any).memory?.usedJSHeapSize
}
})
// パフォーマンス基準をチェック
expect(metrics.duration).toBeLessThan(1000) // 1秒以内
})
制限事項 | 影響 | 回避策 |
---|---|---|
アドレスバーなし | URL同期テスト不可 | location.hrefを直接操作 |
デバッグの難しさ | 開発効率低下 | page.pause()とスクリーンショット活用 |
CSSセレクタ非対応 | 要素選択の制限 | data-testid属性を活用 |
実験的機能 | 破壊的変更の可能性 | バージョン固定とテスト |
CI環境での不安定性 | フレーキーテスト | リトライとタイムアウト調整 |
Browser Mode を導入してから、本番環境でのバグが 80%減少しました。 特に、複雑なフォームバリデーションやリアルタイムチャートの テストが実際のブラウザ環境で実行できるようになったことが大きいです。
Chrome DevToolsとの深い統合
テストケース自動生成
異なるブラウザでの同時テスト
AIによる見た目の差分検出
✅ JSDOM では不十分なテストケースの特定 ✅ チーム全体での Playwright 知識の共有 ✅ ci/cd 環境の準備とコスト見積もり ✅ 段階的移行計画の策定 ✅ パフォーマンス基準の設定
Vitest Browser Mode と Playwright の組み合わせは、2025年のコンポーネントテストにおける最適解の 1 つです。実際のブラウザ環境でテストを実行することで、ユーザーが体験する挙動を正確に検証でき、品質の高いアプリケーション開発が可能になります。
まずは小さなコンポーネントから始めて、徐々に適用範囲を広げていくアプローチをお勧めします。