RSpecのsystem specやfeature specを、SeleniumではなくPlaywrightで動かすには、Capybaraのドライバーとして capybara-playwright-driver を入れ、npmのPlaywrightを同じ版でそろえます。手順そのものは短いものの、Rails 7.1以降では driven_by :playwright と書いた瞬間に自前のドライバー登録が上書きされる、ロケール未設定の環境ではテストが止まったまま進まない、といった詰まりどころがあります。この記事では2026年10月1日時点の最新版(capybara-playwright-driver 0.5.12/playwright-ruby-client 1.63.0/Capybara 3.40.0)を手元で動かした結果をもとに、導入からCIまでを順に説明します。
まとめ:RSpec+Capybara+Playwrightの導入の要点
- Gemfileに
capybara-playwright-driverを追加し、npmのplaywrightをPlaywright::COMPATIBLE_PLAYWRIGHT_VERSION(gem 1.63.0なら1.63.0)と同じ版で入れる。CLIの版を変えてnpx playwright installをやり直さないと、ブラウザの実行ファイルが見つからず全件失敗する。 - Rails 7.1以降のsystem specで
driven_by :playwrightを使うと、Railsが:playwrightを登録し直す。オプションはdriven_byのoptions:に渡すか、:customized_playwrightなど別名で登録する。 have_contentなどが再試行するCapybara.default_max_wait_timeは既定2秒。公式ガイドはPlaywrightドライバーでは15〜30秒を推奨している。- Seleniumからの移行では、confirmダイアログを開く操作を
accept_confirmのブロックで包む書き方に直す。日本語のfill_inではkeydownが発火しない。 LANGが未設定でRubyの外部エンコーディングがUS-ASCIIになる環境では、日本語を含むページでテストが止まる。CIとDockerではLANG=C.UTF-8を設定する。
capybara-playwright-driverの位置づけと関係するgemの役割
capybara-playwright-driverはCapybaraのドライバーの1つで、内部ではplaywright-ruby-clientがNode.jsのPlaywright(playwright run-driver)を子プロセスとして起動し、標準入出力でやり取りします。Rubyだけでは完結せず、Node.jsとnpmのPlaywrightが必要になるのはこのためです。
| パッケージ | 役割 | 2026-10-01時点の最新版 |
|---|---|---|
| capybara(gem) | テストDSL | 3.40.0(2024-01-27公開・Ruby 3.0以上) |
| capybara-playwright-driver(gem) | CapybaraのPlaywrightドライバー | 0.5.12(2026-09-28公開) |
| playwright-ruby-client(gem) | PlaywrightのRubyクライアント | 1.63.0(2026-09-11公開) |
| playwright(npm) | Playwright本体とブラウザ管理 | 1.63.0(Node.js 20以上) |
| rspec-rails(gem) | RSpecのRails統合 | 8.0.4 |
RSpec自体の書き方やrspec-railsの導入はRSpecとは?Rubyのテスト自動化の書き方とrspec-rails導入・RSpec 4移行を解説に、Playwrightの仕組みはPlaywrightとは|仕組み・できること・Seleniumとの違いを解説にまとめています。
Selenium・Cupriteとの比較とドライバーの選び方
RailsのE2Eで候補になるドライバーは3つです。Railsのsystem testは既定で「Seleniumドライバー・Chrome・1400×1400」を使うとRailsガイドに書かれています。
| ドライバー | ブラウザとの通信 | 対応ブラウザ | Node.js |
|---|---|---|---|
| Selenium(selenium-webdriver 4.50.0) | W3C WebDriver | Chrome・Edge・Firefox・Safari | 不要 |
| Cuprite(0.18) | Chrome DevTools Protocol(Ferrum) | Chrome・Chromiumのみ | 不要 |
| capybara-playwright-driver(0.5.12) | Playwrightドライバー経由 | Chromium・Firefox・WebKit | 必要 |
判断の分かれ目はNode.jsを持ち込めるかと、LinuxのCI上でSafari系(WebKit)を確認したいかです。フロントエンドのビルドで既にNode.jsがあるRailsアプリなら、Playwrightを入れる追加負担は小さく、WebKitまで同じコードで回せます。逆に、Node.jsを一切使わないAPI中心のアプリで、Chromeだけ確認できれば十分ならCupriteの方が依存が少なく済みます。Selenium自体の仕組みはSeleniumとは?WebDriverの仕組みと4系の実行手順・ツール選定を解説を参照してください。
Gemfileとnpmパッケージの導入手順
Gemfileへの追加とbundle install
テスト用グループに追加します。capybara-playwright-driverはplaywright-ruby-clientに依存しているので、後者を明示しなくても入ります。Selenium系のgemは不要です。
group :test do
gem "capybara"
gem "capybara-playwright-driver"
end
bundle install
bundle exec ruby -e 'require "playwright"; puts Playwright::COMPATIBLE_PLAYWRIGHT_VERSION'
# => 1.63.0
npmのPlaywrightを互換版でそろえる
npm側は、gemが返す COMPATIBLE_PLAYWRIGHT_VERSION と同じ版を入れるのが公式手順です(公式ガイドは playwright-core を入れる手順で、playwright パッケージでも動きます)。@latest で入れると、gemが想定しない版のドライバーで動くことになります。
npm install --save-dev [email protected]
npx playwright install chromium
どの版のブラウザを使うかは、gemではなくnpmのPlaywright CLI側が決めます。CLIの版を変えたのにブラウザを入れ直さないと、次の食い違いが起きます。手元でplaywright-ruby-client 1.63.0のまま、CLIだけを1.62.0に差し替えて実行したところ、4件すべてが visit の時点で次のエラーになりました。1.63.0のCLIが入れたのは chromium_headless_shell-1243 で、1.62.0は -1234 を探しに行くためです。
Playwright::Error:
Executable doesn't exist at ~/Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-x64/chrome-headless-shell
gemを上げたら package.json の版も同じ数字にし、npx playwright install をやり直します。Playwrightの版の追い方はPlaywrightの最新バージョンは1.63|確認・更新方法とリリース履歴【2026年9月】で扱っています。
ブラウザの保存先とPLAYWRIGHT_BROWSERS_PATH
ブラウザはOSごとのキャッシュフォルダに入ります(macOSは ~/Library/Caches/ms-playwright、Linuxは ~/.cache/ms-playwright、Windowsは %USERPROFILE%\AppData\Local\ms-playwright)。環境変数 PLAYWRIGHT_BROWSERS_PATH で場所を変えられ、0 を指定すると node_modules 配下に置かれます。ヘッドレスで動かすときのChromiumは通常版ではなく chromium_headless_shell が使われるため、CIでヘッドレスしか使わないなら --only-shell で通常版のダウンロードを省けます。インストール自体が失敗する場合の対処はPlaywrightのインストール方法とできない時の対処|バージョン確認・Windows・Pythonを参照してください。
ドライバー登録とRSpecの設定
register_driverの書き方と主なオプション
Railsを使わない素のRSpec(feature spec)なら、spec_helper.rb で登録して既定のドライバーにします。以下は手元で4件のspecを通した設定をもとにしたものです。
require "capybara/rspec"
require "capybara-playwright-driver"
Capybara.app = MyRackApp # Railsでなければ対象のRackアプリを指定
Capybara.server = :puma, { Silent: true }
Capybara.register_driver(:playwright) do |app|
Capybara::Playwright::Driver.new(app,
browser_type: :chromium, # :firefox / :webkit も可
headless: true,
playwright_cli_executable_path: "./node_modules/.bin/playwright")
end
Capybara.default_driver = :playwright
Capybara.javascript_driver = :playwright
Capybara.default_max_wait_time = 15
渡せるオプションは、ドライバー固有のもの、BrowserType#launch に渡るもの、Browser#new_context に渡るものの3種類に、リモート接続用のキーが加わります。0.5.12のソースでは、browser_options.rb と page_options.rb の許可リストにないキーは黙って捨てられます。
| 区分 | 主なキー |
|---|---|
| ドライバー | playwright_cli_executable_path・browser_type・default_timeout |
| launch | headless・channel・slowMo・args・executablePath・proxy |
| new_context | viewport・locale・timezoneId・record_video_dir・storageState |
| リモート接続 | browser_server_endpoint_url・playwright_server_endpoint_url |
playwright_cli_executable_path の既定値は npx playwright です。node_modules の実行ファイルを直接指すと npx の解決処理が省けるうえ、版を指定しない npx が互換外の版を取りに行く事故も防げます。同じ4件を2回ずつ流した結果は、既定値で13.90秒と13.93秒、直接指定で12.30秒と12.12秒でした(macOS・Intel、ブラウザ起動を含む)。
system specのdriven_by :playwrightで登録が上書きされる問題
Railsのsystem specで最初に当たりやすい詰まりどころです。Rails 7.1から、ActionDispatch::SystemTesting::Driver が :selenium・:cuprite・:rack_test に加えて :playwright を自前で登録するようになりました(7.0.0のソースには playwright の文字列がありません)。driven_by :playwright を呼ぶたびにRailsが Capybara.register_driver(:playwright) をやり直すため、rails_helper.rb で同じ名前に付けたオプションは消えます。
actionpack 8.1.4で確かめると、先に browser_type: :firefox で :playwright を登録しても、driven_by :playwright 相当の処理の後はChromiumに戻っていました。回避策は2つです。
# 方法1:Railsの組み込み登録に options: で渡す(screen_sizeはviewportとscreenに入る)
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :playwright, screen_size: [1400, 1400],
options: { browser_type: :firefox, headless: true, name: :playwright_firefox }
end
end
# 方法2:別名で登録し、その名前でdriven_byする
Capybara.register_driver(:customized_playwright) do |app|
Capybara::Playwright::Driver.new(app, browser_type: :chromium, headless: true)
end
RSpec.configure do |config|
config.before(:each, type: :system) { driven_by :customized_playwright }
end
方法1の name: はRailsが登録名として取り出すキーで、ドライバーには渡りません。複数のブラウザ設定を並べるなら方法1、Capybara.default_driver にも使い回すなら方法2です。なお、playwright-ruby-clientのガイドは予約の開始を「Rails 6.1以降」と書いていますが、ソース上で :playwright が入ったのは7.1です。
Capybara.default_max_wait_timeの設定値
Capybaraの have_content や find は、条件を満たすまで default_max_wait_time(既定2秒)だけ再試行します。初回の visit はドライバーとブラウザの起動が乗って9.8秒かかりましたが、visit はページの読み込みを別枠で待つので、既定2秒のままでも手元の4件は通りました。2秒が効くのは have_content などの再試行で、API応答や描画が2秒を超える画面ではここで失敗します。公式ガイドは15〜30秒を推奨しています。ページ遷移の待ちだけを延ばしたいときは、ドライバーの default_navigation_timeout(秒)を使えます。
specの書き方とPlaywrightのAPIを直接使う方法
フォーム送信・非同期表示・ダイアログのテスト例
Capybara DSLはSeleniumのときと同じ書き方で動きます。次の3件は手元で通ったspec(4件目は次節の with_playwright_page)で、2件目はボタン押下から1.5秒後に表示される文言を待っています。
RSpec.describe "Todo", type: :feature do
it "フォーム送信" do
visit "/"
fill_in "タイトル", with: "牛乳を買う"
click_button "追加"
expect(page).to have_content "登録しました: 牛乳を買う"
end
it "1.5秒後に出る要素を待つ" do
visit "/"
click_button "読み込む"
expect(page).to have_css "#out", text: "読み込み完了"
end
it "confirmダイアログを承認" do
visit "/"
accept_confirm("削除しますか?") { click_button "削除" }
expect(page).to have_content "削除しました"
end
end
Railsアプリのsystem specでは type: :system にし、前節の driven_by を使います。APIの入出力だけを確かめたい部分はブラウザを起動しないrequest specとは?Rails 8・RSpec 8でのリクエストスペックの書き方に寄せると、E2Eの本数を絞れます。配分の考え方はテストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計が参考になります。
with_playwright_pageによるPlaywright locatorの直接操作
Capybara DSLで書きにくい操作は、page.driver.with_playwright_page のブロックで Playwright::Page を直接扱えます。要素単位では with_playwright_element_handle もあります。
page.driver.with_playwright_page do |pw|
expect(pw.locator("h1").text_content).to eq "Todo"
pw.get_by_label("本文").fill(long_text)
end
操作を共通化する設計はPage Object Model(POM)とは?Playwright・Seleniumの実装例と保守コストの実測で、Playwright側の実装例と合わせて説明しています。
Seleniumから移行すると落ちるテストと直し方
ドライバーを差し替えただけで落ちるテストは、ダイアログの扱い・キーイベント・無効要素のクリック・表示テキストの4点に集中します。多くはテスト側を直しますが、keydownに依存した画面はアプリ側を直します。
confirmダイアログのブロック形式での承認
公式ガイドによると、Selenium経由では開いたままのダイアログを後から accept_confirm で処理できる場合がありますが、この「クリックしてから accept_confirm」の順番はPlaywrightでは動きません。手元で試すと、クリックは0.5秒で戻り、[WARNING] Unexpected modal - "削除しますか?" が出てダイアログはキャンセル扱いで閉じられ、続く accept_confirm は NoMethodError で落ちました。accept_confirm・accept_alert・dismiss_confirm・accept_prompt は、ダイアログを開く操作をブロックで包む形に統一します。
# 落ちる
click_button "削除"
accept_confirm("削除しますか?")
# 通る
accept_confirm("削除しますか?") { click_button "削除" }
日本語fill_inでのkeydown不発火
READMEによると、fill_in は1文字ずつのタイピングで入力し、USキーボードにない文字は input イベントだけを送ります。入力欄でイベントを数えた結果は次のとおりです。
| 入力方法 | keydown | input |
|---|---|---|
| fill_in “abc” | 3回 | 3回 |
| fill_in “牛乳です” | 0回 | 4回 |
| Playwrightのfill(“牛乳です”) | 0回 | 1回 |
keydown や keyup で文字数カウントや入力チェックを動かしている画面は、英数字のテストでは通っても日本語のテストでは表示が変わりません。アプリ側を input イベントで拾う実装に直すのが本筋です。長文を入れるだけなら、1回の input で済む fill の方が速く終わります。
無効ボタンの二度押しと非表示brのテキスト
二重送信防止のテストで、取得済みのボタン要素(find(:button, "送信", disabled: true) など)をもう一度 .click すると、Selenium経由では何も起きずに戻ることがありますが、Playwrightは押せる状態になるまで待ってタイムアウトします。click_button はそもそも無効なボタンを探さないため、どのドライバーでも Capybara::ElementNotFound になります。二度押しせず、expect(button).to be_disabled で無効化そのものを確かめます。もう1つは表示テキストの差で、Playwrightはブラウザの innerText に従うため、CSSで隠した <br> は改行になりません。Seleniumが "foo\nbar" を返す箇所が、Playwrightでは "foobar" になります。normalize_ws: true は空白をまとめるだけで空白を足さないので、この差は吸収できません。前後の文言を別々に have_text で確かめるか、have_text(/foo\s*bar/) の正規表現にします。空行や段落境界の差だけなら normalize_ws: true で吸収できます。
失敗原因を追うトレース・スクリーンショット・動画
Seleniumからの乗り換えで増える機能のうち、実務で効くのは失敗時の記録です。capybara-playwright-driverは3種類の保存フックを持っています。
on_save_trace:テストごとにPlaywrightのトレース(zip)を保存し、パスをブロックに渡すon_save_raw_screenshot_before_reset:セッションのリセット直前の画面をPNGのバイナリで渡すon_save_screenrecord:record_video_dirを指定しなくても、録画した動画のパスを渡す
RSpec.configure do |config|
config.before(:each, type: :system) do |example|
driven_by :playwright
FileUtils.mkdir_p("tmp/traces")
Capybara.current_session.driver.on_save_trace do |zip_path|
FileUtils.cp(zip_path, "tmp/traces/#{example.location.parameterize(separator: "_")}.zip")
end
end
end
フックの登録は driven_by の後に書きます。先に走ると current_session.driver がrack_testのままで、on_save_trace が NoMethodError になります。ファイル名に full_description.parameterize を使うと日本語部分が消えて "todo" のように重複するため、example.location から作ります。特定の区間だけ記録したいときは page.driver.trace(path: "tmp/trace.zip", screenshots: true, snapshots: true) { ... } を使います。手元ではページ表示とボタン操作の1件で30,752バイトのzipができました。中身は npx playwright show-trace tmp/trace.zip で、操作ごとのDOMスナップショットとネットワークを時系列で確認できます。ガイドには with_trace という名前が出てきますが、0.5.12のソースにあるメソッド名は trace です。画面を見ながら原因を探したいときは、ドライバーに headless: false, slowMo: 100 を渡すと操作が1つずつ遅れて実行されます。CLIの他のサブコマンドはPlaywright CLIコマンド一覧|test・codegen・installと1.63の新オプションにまとめています。
DockerとGitHub Actionsで動かす設定
LANG未設定の環境でテストが止まる問題
CIで最初に確認すべき点です。LANG も LC_ALL も空の環境では、Rubyの Encoding.default_external がUS-ASCIIになります。この状態で日本語を含むページを開くと、playwright-ruby-clientの受信スレッドが JSON.parse で Encoding::InvalidByteSequenceError を出して終了し、メインのテストは応答を待ったまま止まります。手元では5分待っても進まず、プロセスを手動で止めました。
対処は LANG=C.UTF-8 を設定するか、spec_helper.rb の先頭で Encoding.default_external = Encoding::UTF_8 を指定することです。どちらでも4件が通りました。Playwright公式のDockerイメージは設定済みですが、自前のイメージやcron・launchd経由の実行では空になりやすいので、CIに明示しておきます。
Docker:公式イメージとリモートブラウザ接続
Playwright公式イメージ mcr.microsoft.com/playwright:v1.63.0-noble(Ubuntu 24.04)には、ブラウザと依存ライブラリが入っていて、PLAYWRIGHT_BROWSERS_PATH=/ms-playwright が設定されています。npmのPlaywrightパッケージは含まれません。公式ドキュメントは、Chromiumのメモリ不足を避けるための --ipc=host と、ゾンビプロセス対策の --init を推奨しています。
Railsのコンテナとブラウザのコンテナを分ける場合は、ブラウザ側でサーバーを起動し、Ruby側は browser_server_endpoint_url で接続します。ブラウザからRailsのテストサーバーへ届くよう、Capybaraのサーバーを 0.0.0.0 で待ち受けさせます。
# compose.yaml
services:
playwright:
image: mcr.microsoft.com/playwright:v1.63.0-noble
command: >
/bin/sh -c "npm install --no-save [email protected] &&
./node_modules/.bin/playwright-core run-server --port 3000 --host 0.0.0.0 --path /ws"
init: true
ipc: host
Capybara.register_driver(:playwright_remote) do |app|
Capybara::Playwright::Driver.new(app,
browser_type: :chromium,
headless: true,
browser_server_endpoint_url: "ws://#{ENV.fetch('PLAYWRIGHT_HOST')}:3000/ws")
end
Capybara.server_host = "0.0.0.0"
Capybara.server_port = 4000
Capybara.app_host = "http://#{ENV.fetch('APP_HOST')}:4000"
GitHub Actionsのワークフロー例
ヘッドレスのChromiumだけで回すなら、--only-shell で通常版Chromiumのダウンロードを省けます。--with-deps はUbuntuに必要なライブラリも入れます。
jobs:
system-spec:
runs-on: ubuntu-latest
env:
LANG: C.UTF-8
RAILS_ENV: test
steps:
- uses: actions/checkout@v7
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npx playwright install --with-deps --only-shell chromium
- run: bundle exec rspec spec/system
headless: false や channel: "chromium"(新ヘッドレス)では通常版Chromiumが要るので、--only-shell を外します。channel: "chrome" はGoogle Chrome本体を使うため、別途 npx playwright install chrome を実行します。DBを使うRailsアプリでは、rspec の前にDBのサービスコンテナと bin/rails db:prepare の手順も必要です。
よくある質問
capybara-playwright-driverとplaywright-ruby-clientはどう違いますか?
playwright-ruby-clientはPlaywrightをRubyから操作するクライアントで、capybara-playwright-driverはそれをCapybaraのドライバーとして包んだgemです。RSpecでCapybara DSLを使うなら後者を入れれば前者も依存として入ります。Capybaraを使わずPlaywrightのAPIだけで書く場合は、playwright-ruby-client単体でも動かせます。
Railsのsystem testは標準でPlaywrightを使えますか?
Rails 7.1以降は driven_by :playwright に対応していますが、Railsのsystem testの既定はSelenium・Chrome・1400×1400のままです(rspec-rails 8.0.4のsystem specで driven_by を書かない場合は :selenium_chrome_headless)。Gemfileにcapybara-playwright-driverを追加し、npmのPlaywrightを入れてから driven_by :playwright に切り替えます。
FirefoxやWebKitでもテストできますか?
できます。browser_type: :firefox または :webkit を指定し、npx playwright install firefox webkit でブラウザを入れます。Rails 7.1以降の driven_by では options: { browser_type: :webkit } の形で渡します。
npx playwright installはいつ実行し直す必要がありますか?
npmのPlaywrightの版を変えたとき(上げ下げとも)です。ブラウザのリビジョン(Chromiumなら1.63.0で1243)は版ごとに決まっていて、古いブラウザでは起動せずエラーになります。保存先を変えたいときは PLAYWRIGHT_BROWSERS_PATH を設定してから実行します。
Capybara.default_max_wait_timeはいくつにすべきですか?
既定の2秒では、API応答や非同期描画を待つアサーションが失敗しやすくなります。playwright-ruby-clientの公式ガイドは15〜30秒を推奨しています。条件を満たした時点で次に進むので、成功するテストは遅くなりません。