Capybara::Lightpanda
A Capybara driver for Lightpanda, the fast headless browser built in Zig.
This gem provides a self-contained, production-ready Capybara driver with a built-in CDP client. No external browser-client gem required — just install and go:
- Reliable navigation — falls back to
document.readyStatepolling whenPage.loadEventFireddoesn't fire (a known Lightpanda limitation on pages with complex JS) - XPath polyfill — auto-injected after each navigation so Capybara's internal XPath selectors work (
find,click_on,fill_in,assert_selector, etc.) - Cookie management —
set_cookie,clear_cookies,remove_cookieon the driver + graceful fallback whenNetwork.clearBrowserCookiescrashes the CDP connection - Drop-in Capybara integration — registers a
:lightpandadriver, configure and go
Architecture
Similar to how Cuprite builds on Ferrum, but as a single gem:
Capybara → capybara-lightpanda (driver + CDP client) → Lightpanda browser
Installation
1. Install the Lightpanda browser
# macOS
brew install lightpanda-io/lightpanda/lightpanda
# Linux (Debian/Ubuntu) — see https://lightpanda.io/docs/
2. Add the gem
# Gemfile
group :test do
gem "capybara-lightpanda"
end
bundle install
Usage
Basic setup
# test/support/capybara.rb or spec/support/capybara.rb
require "capybara-lightpanda"
Capybara::Lightpanda.configure do |config|
config.host = "127.0.0.1"
config.port = 9222
config.timeout = 15
config.browser_path = "/usr/local/bin/lightpanda" # optional, auto-detected
end
Capybara.default_driver = :lightpanda
Capybara.javascript_driver = :lightpanda
Dual-driver setup (recommended)
Run most tests with Chrome, use Lightpanda for fast DOM-only tests:
if ENV["BROWSER"] == "lightpanda"
require "capybara-lightpanda"
Capybara::Lightpanda.configure do |config|
config.timeout = 15
end
Capybara.default_driver = :lightpanda
Capybara.javascript_driver = :lightpanda
else
# Your existing Chrome/Cuprite setup
Capybara.default_driver = :cuprite
end
# Run with Lightpanda
BROWSER=lightpanda bundle exec rails test test/system/
# Run with Chrome (default)
bundle exec rails test test/system/
Setting cookies (e.g. login helper)
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
def login_as(user)
session = user.sessions.first_or_create!
= ActionDispatch::TestRequest.create({ "REQUEST_METHOD" => "GET" }).
.signed[:session_id] = { value: session.id }
page.driver.(
"session_id",
[:session_id],
domain: "127.0.0.1",
httpOnly: true,
secure: false,
)
end
end
What works
- Navigation (
visit,click_link,go_back,go_forward,refresh) - JavaScript execution (V8 engine) —
evaluate_script,execute_script,evaluate_async_script - Forms —
fill_in,click_button,select,choose,check,uncheck - Finding —
find,all,within, CSS and XPath selectors - Matchers —
assert_selector,assert_text,assert_current_path,has_field?,has_select? - Cookies — set/get/clear/remove via CDP
- Frames —
within_frame, scoped finding - Keyboard —
send_keyswith modifiers and special keys - Network — traffic tracking, custom headers, idle waiting
Turbo Rails support
The gem handles Turbo-enabled Rails apps transparently:
| Feature | Status | How |
|---|---|---|
| Turbo Frames | Works natively | Lazy-loading (src=), scoped link navigation |
| Turbo Drive | Auto-disabled | Gem disables Drive (body replacement fails in Lightpanda) — standard link navigation restored |
| Form submission | Auto-handled | When Turbo is present, forms submit via fetch() + document.write() to bypass Turbo's interception |
| Turbo Streams | Not supported | Depends on Turbo's fetch pipeline which Lightpanda can't render |
Root cause: Lightpanda's document.body is read-only — Turbo Drive's body replacement and frame form responses can't be applied. The gem works around this automatically.
Known limitations
These are Lightpanda browser limitations, not driver limitations:
| Feature | Status |
|---|---|
| Screenshots | Not supported (no rendering engine) |
window.getComputedStyle() |
Returns defaults (no CSS engine) |
scroll_to, resize |
No layout engine |
| Complex Stimulus controllers | Some may not execute fully |
| XPath axes/functions | Polyfill covers ~80% of Capybara usage |
| File uploads | Not yet supported |
| Turbo Streams | Not supported (Turbo's fetch-then-render pipeline) |
Benchmark
Tested on a Rails 8.1 app (Turbo + Stimulus), 24 DOM-only tests:
| Driver | Tests | Time | Speed |
|---|---|---|---|
| Lightpanda | 24/24 pass | 6.89s | 3.48 tests/s |
| Chrome | 24/24 pass | 7.09s | 3.38 tests/s |
Lightpanda's advantage is expected to grow on larger suites due to faster startup and lower memory usage.
Configuration
Capybara::Lightpanda.configure do |config|
config.host = "127.0.0.1" # Lightpanda bind host
config.port = 9222 # Lightpanda CDP port
config.timeout = 15 # Navigation/command timeout (seconds)
config.process_timeout = 10 # Browser process startup timeout
config.browser_path = nil # Path to lightpanda binary (auto-detected)
end
Dynamic port (parallel tests)
def available_port
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server.close
port
end
Capybara::Lightpanda.configure do |config|
config.port = ENV.fetch("LIGHTPANDA_PORT", available_port).to_i
end
How it works
| Component | Description |
|---|---|
Browser |
High-level API with readyState polling fallback when Page.loadEventFired never fires |
Cookies |
Catches BrowserError from unsupported Network.clearBrowserCookies, deletes cookies individually |
XPathPolyfill |
Provides document.evaluate + XPathResult shim for Capybara's XPath selectors |
Client |
CDP command dispatch over WebSocket with timeout and event subscription |
Driver |
Complete Capybara driver with set_cookie, clear_cookies, remove_cookie |
Node |
DOM interactions via JavaScript evaluation |
Credits
- Lightpanda — the headless browser
- Capybara — the test framework
- Inspired by the Cuprite / Ferrum architecture and
lightpanda-ruby
Patterns adapted from these MIT-licensed projects (cookies API, frame switching, node call/error conventions, retry/event utilities) are acknowledged with the original copyright notices in NOTICE.md.
Contributing
Bug reports and pull requests are welcome on GitHub.