Reference

Documentation

Install, configure, and run capybara-lightpanda — the Capybara driver for the Lightpanda headless browser.

← Back to overview README on GitHub ↗

Quick start

1. Install the Lightpanda browser

# macOS
brew install lightpanda-io/lightpanda/lightpanda

# Linux — download the static binary from the release page
curl -L https://github.com/lightpanda-io/browser/releases/latest/download/lightpanda-x86_64-linux \
  -o /usr/local/bin/lightpanda
chmod +x /usr/local/bin/lightpanda

Verify the install:

lightpanda --version

2. Add the gem

# Gemfile
group :test do
  gem "capybara-lightpanda"
end
bundle install

3. Register the driver

# spec/support/capybara.rb (or test/support/capybara.rb)
require "capybara-lightpanda"

Capybara::Lightpanda.configure do |config|
  config.host = "127.0.0.1"
  config.port = 9222
  config.timeout = 15
end

Capybara.default_driver = :lightpanda
Capybara.javascript_driver = :lightpanda

That’s it. Run your suite and the driver will boot a Lightpanda process and connect over CDP.

Configuration

Capybara::Lightpanda.configure do |config|
  config.host = "127.0.0.1"   # Lightpanda bind host
  config.port = 9222          # CDP port
  config.timeout = 15         # navigation/command timeout (seconds)
  config.process_timeout = 10 # browser startup timeout
  config.browser_path = nil   # path to lightpanda binary; nil = auto-detect
end
OptionDefaultNotes
host"127.0.0.1"Bind address for the CDP server
port9222TCP port; use a dynamic port for parallel suites
timeout15Per-CDP-command timeout, also covers navigation polling
process_timeout10Wait this long for lightpanda serve to start before failing
browser_pathnilIf nil, the driver searches PATH and common Homebrew paths

Dynamic port for 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

Setup recipes

Single driver (Lightpanda everywhere)

For projects that don’t depend on rendered visuals:

require "capybara-lightpanda"

Capybara.default_driver = :lightpanda
Capybara.javascript_driver = :lightpanda

Dual driver (Cuprite + Lightpanda)

Keep Cuprite for visual specs (anything that takes screenshots or asserts on pixels) and route the rest through Lightpanda:

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
  # existing Cuprite setup
  Capybara.default_driver = :cuprite
end
# fast headless run
BROWSER=lightpanda bundle exec rspec spec/system/

# default (Chrome via Cuprite)
bundle exec rspec spec/system/

Login helper via cookies

Set a session cookie before navigating, so you don’t have to drive the login form on every spec:

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  def login_as(user)
    session = user.sessions.first_or_create!
    cookie_jar = ActionDispatch::TestRequest
      .create({ "REQUEST_METHOD" => "GET" })
      .cookie_jar
    cookie_jar.signed[:session_id] = { value: session.id }

    page.driver.set_cookie(
      "session_id",
      cookie_jar[:session_id],
      domain: "127.0.0.1",
      httpOnly: true,
      secure: false,
    )
  end
end

What works

Capybara surfaceStatus
Navigation — visit, click_link, go_back, go_forward, refresh
JavaScript — evaluate_script, execute_script, evaluate_async_script✓ (V8)
Forms — fill_in, click_button, select, choose, check, uncheck
Finders — find, all, within, CSS + XPath✓ (XPath via polyfill)
Matchers — assert_selector, assert_text, has_field?, has_select?
Cookies — set_cookie, clear_cookies, remove_cookie
Frames — within_frame, scoped finding
Keyboard — send_keys with modifiers

Turbo Rails

The driver handles Turbo-enabled Rails apps transparently.

FeatureStatusMechanism
Turbo FramesNativeLazy-load (src=) and scoped link navigation use Turbo’s existing fetch + innerHTML swap
Turbo DriveNativeLightpanda’s body.replaceWith works since v0.2.9; the driver’s selector polyfill keeps #id lookups working through the snapshot+swap pattern
Form submissionAuto-handledfetch() + document.write() shim bypasses Turbo’s interception when needed
Turbo StreamsNot supportedLightpanda lacks the rendering pipeline Streams depend on

Known limitations

These are upstream Lightpanda limits, not driver bugs:

SurfaceStatus
ScreenshotsNot supported — no rendering engine
window.getComputedStyle()Returns defaults — no CSS engine
scroll_to, resizeNo layout engine
File uploads (<input type="file">)Not yet supported (upstream #2175)
Complex Stimulus controllersSome may not execute fully
XPath axes / functionsPolyfill covers ~80% of Capybara’s usage

If you need any of these, run that spec under Cuprite and keep the rest on Lightpanda.

How it works

ComponentResponsibility
Capybara::Lightpanda::BrowserHigh-level page API; falls back to document.readyState polling when Page.loadEventFired is unreliable
Capybara::Lightpanda::ClientCDP command dispatch over WebSocket with timeouts and event subscription
Capybara::Lightpanda::DriverThe Capybara driver — registers as :lightpanda, exposes set_cookie / clear_cookies / remove_cookie
Capybara::Lightpanda::NodeDOM operations via Runtime.callFunctionOn with object-id binding
Capybara::Lightpanda::CookiesWraps Network.getCookies / setCookie / deleteCookies with safe fallbacks
javascripts/index.jsXPath polyfill, Turbo activity tracking, requestSubmit polyfill, #id selector rewrite for Lightpanda’s CSS-engine quirk

The driver speaks the same CDP dialect Cuprite and Ferrum use, so most patterns from those projects translate directly. Where Lightpanda diverges from Chromium, the driver papers over it.

Examples

Runnable Rails demos in the repo, covering both RSpec and Minitest, with and without Turbo:

Reference