CapybaraからPlaywrightへ:ブラウザテスト基盤の全面移行

basecamp/lexxy

Lexxyのブラウザテスト基盤をCapybara/SeleniumからPlaywrightへ移行し、ViteのDevServerをテストホストとして採用しました。テスト実行時間が約20秒超から約6秒へと短縮され、ChromiumのみだったブラウザカバレッジがFirefoxとWebKitにまで拡大されています。

背景

Capybaraベースのシステムテストは、Railsの起動・データベース準備・Seleniumの制約という三重のオーバーヘッドを抱えていました。ブラウザカバレッジはSelenium経由のChromeのみに限定され、クリップボード操作やキーボードフォーカスといったネイティブブラウザ挙動の再現精度が低く、escape_formatやツールバーキーボードナビゲーションのテストではSeleniumのタイミング起因のフレーキーな失敗が発生していました。

Playwrightへの移行を可能にした鍵は「テストの関心分離」です。本PRでは、Railsが本当に必要なテスト(Action Textレンダリング・Active Storage統合・サーバーサイドSGID解決)のみをCapybaraシステムテストとして残し、それ以外のエディタ挙動テストをすべてPlaywrightへ移行するという明確な線引きが行われました。この分離により、ブラウザテストの実行にRuby・Bundler・データベースが不要になりました。

技術的な変更

Vite DevServerによるHTMLフィクスチャ

テスト環境のホストをRails dummy appからVite DevServerへ切り替え、test/browser/fixtures/以下に10種類の静的HTMLページが新設されました。各ページはエディタの設定パターン(indexautofocusmarkdown-disabledrich-text-disabledsingle-linetoolbar-disabledtoolbar-externalattachments-disabledattachments-enabledattachments-invalid)に対応します。

各HTMLフィクスチャはtest/browser/fixtures/editor.js経由でViteのエイリアスを通じてsrc/index.jsをインポートし、app/assets/stylesheets/のCSSを直接参照します。Stimulusコントローラに依存していたイベント追跡は、events_logger.jsがフォームレベルでlexxy:focuslexxy:changeなど9種類のカスタムイベントをリッスンしてDOMに記録するシンプルな実装に置き換えられました。

test/browser/vite.config.jsではlexxy解決エイリアスとフィクスチャルートが設定され、playwright.config.jswebServerディレクティブがViteプロセスを自動起動・待機します。

Active Storageアップロードのモック

Active Storageの直接アップロードフローをRailsサーバーなしで完結させるため、test/browser/helpers/active_storage_mock.jsが新設されました。mockActiveStorageUploads(page)page.route()を使って2つのエンドポイントをインターセプトします。

  • POST /rails/active_storage/direct_uploads: blobレコード作成リクエストを受け取り、mock-signed-id-N形式の署名済みIDを含む現実的なJSONレスポンスを返す
  • PUT `/rails/active_storage/disk/`**: ファイルバイト書き込みリクエストを受け取り、204を返す
export async function mockActiveStorageUploads(page) {
  let blobCounter = 0
  const calls = { blobCreations: [], fileUploads: [] }

  await page.route("**/rails/active_storage/direct_uploads", async (route) => {
    const body = JSON.parse(route.request().postData())
    const blob = body.blob
    const signedId = `mock-signed-id-${++blobCounter}`
    calls.blobCreations.push(blob)
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({ signed_id: signedId, /* ... */ }),
    })
  })

  await page.route("**/rails/active_storage/disk/**", async (route) => {
    if (route.request().method() !== "PUT") return route.fallback()
    await route.fulfill({ status: 204 })
  })
}

このモックは@rails/activestorage(devDependencyとして追加)の実際のアップロードフローを全ブラウザで完走させます。テストごとにカウンターが独立しており、共有状態は持ちません。

EditorHandleとアサーションヘルパー

test/browser/helpers/editor_handle.jsEditorHandleクラスが新設され、RubyのEditorHandlerに対応するJavaScript実装として機能します。value()setValue()send()focus()uploadFile()などのメソッドがエディタ要素をラップします。

send()の実装では、各キー送信後にflush()を呼び出してLexicalの更新サイクルが完了するまで待機する設計が採用されています。これがescape_formatのタイミング問題を解消した核心です。またtest/browser/helpers/assertions.jsではassertEditorHtml()assertEditorContent()assertEditorPlainText()といったPollingベースのアサーション群が提供され、非同期なエディタ状態変化への対応が統一されています。

CIジョブの再構成

.github/workflows/ci.ymlが大幅に再構成され、以下の4ジョブが並列実行されるようになりました:

  • lint: ESLintとRuboCop
  • js-unit-tests: VitestによるJSユニットテスト(Node環境のみ)
  • rails-system-tests: Rails + Chrome + データベースが必要なCapybaraシステムテスト
  • Playwright browser testsmatrix: [chromium, firefox, webkit]): Node + Playwrightのみ

PlaywrightジョブはRubyもデータベースも不要なため、セットアップが即時完了します。

設計判断

「必要最小限のみRailsに残す」という原則が、この移行の設計軸です。Capybaraシステムテストとして残されたのは、Action Textによるサーバーサイドレンダリング検証・Active Storageプレビュー生成・認証済みストレージ・SGID解決という、Railsスタック全体を実際に動かさなければ検証できないケースのみです。

Viteをテストホストに採用したことで、テストフィクスチャがRailsのアセットパイプラインから独立しました。各HTMLフィクスチャがエディタの設定パターンに1対1で対応する構造は、テスト失敗時の問題箇所の特定を容易にします。

CI環境でのretries: 1workers: 1に対してローカルではretries: 2workers: undefined(並列)という非対称な設定は、CI環境での確定的な再現性とローカルでの高速フィードバックを両立させる判断です。

まとめ

本PRは、エディタ挙動テストの実行環境をRailsスタックから切り離すことで、マルチブラウザ対応・高速化・安定化を同時に達成した移行です。「Railsが本当に必要なテストのみRailsで動かす」という関心分離の原則が、依存関係とテスト実行時間の両方を削減する設計判断の根拠となっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
e80c203f

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景、技術的な変更、設計判断(各論)、まとめ(結論)の3部構成が明確に守られており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:test/browser/helpers/active_storage_mock.js)とPR番号のリンク記法([PR #810](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Capybara, Playwright, Vite, CI設定など、専門的なトピックを前提知識を持つエンジニア向けに適切に記述しており、過度な説明がありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各パラグラフはトピックセンテンスで始まっています。1段落1トピックの原則も守られており、非常に読みやすいです。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

引用されているコードブロック(`test/browser/helpers/active_storage_mock.js`)はDiffに存在しますが、元のコードから一部のロジック(レスポンスボディの詳細、`calls.fileUploads.push`)が省略されています。技術的理解を大きく妨げるものではありませんが、省略の度合いがやや大きいです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Playwright, Vite DevServer, `page.route()`, SGID, Lexicalなど、PRで言及されている技術用語を文脈に沿って正確に使用しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

テスト実行時間の短縮、ブラウザカバレッジの拡大、`flush()`によるタイミング問題の解消など、全ての技術的な説明がPR情報とDiffによって裏付けられており、正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の全ての主張(テスト時間、CIジョブ構成、非対称なリトライ設定など)がPR DescriptionやDiff内容と一致しており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#810)、テスト実行時間(約20秒→約6秒)、フィクスチャの数(10種類)など、すべての数値と固有名詞が正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「CapybaraからPlaywrightへ:ブラウザテスト基盤の全面移行」は、PRの主題「Migrate tests from Capybara to Playwright」を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれない外部知識(バージョンのサポート状況、リリース日程など)の記載はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

PRで完了した「移行」という事実に対し、「移行し」「短縮され」といった過去・完了形の表現が正しく使われており、時間表現に歪曲はありません。