Class: Capybara::Lightpanda::Driver

Inherits:
Driver::Base
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/capybara/lightpanda/driver.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Driver

Returns a new instance of Driver.



15
16
17
18
19
20
21
# File 'lib/capybara/lightpanda/driver.rb', line 15

def initialize(app, options = {})
  super()
  @app = app
  @options = options
  @browser = nil
  @started = false
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



11
12
13
# File 'lib/capybara/lightpanda/driver.rb', line 11

def app
  @app
end

#optionsObject (readonly)

Returns the value of attribute options.



11
12
13
# File 'lib/capybara/lightpanda/driver.rb', line 11

def options
  @options
end

Instance Method Details

#accept_modal(type, **options, &block) ⇒ Object

find_modal owns the wait default (browser.options.timeout) — pass wait only when the caller overrode it.



258
259
260
261
262
# File 'lib/capybara/lightpanda/driver.rb', line 258

def accept_modal(type, **options, &block)
  browser.accept_modal(type, text: options[:with])
  block&.call
  browser.find_modal(type, **{ text: options[:text], wait: options[:wait] }.compact)
end

#active_elementObject



70
71
72
73
# File 'lib/capybara/lightpanda/driver.rb', line 70

def active_element
  oid = browser.active_element
  oid && Node.new(self, oid)
end

#add_headers(headers) ⇒ Object



307
308
309
# File 'lib/capybara/lightpanda/driver.rb', line 307

def add_headers(headers)
  browser.network.add_headers(headers)
end

#browserObject



23
24
25
26
# File 'lib/capybara/lightpanda/driver.rb', line 23

def browser
  @browser = nil if @browser && !browser_alive?
  @browser ||= Browser.new(@options)
end

#browser_alive?Boolean

Returns:

  • (Boolean)


28
29
30
# File 'lib/capybara/lightpanda/driver.rb', line 28

def browser_alive?
  !@browser.nil? && @browser.alive?
end

#clear_cookiesObject



151
152
153
# File 'lib/capybara/lightpanda/driver.rb', line 151

def clear_cookies
  browser.cookies.clear
end

#close_window(_handle) ⇒ Object

Raises:

  • (Capybara::NotSupportedByDriverError)


248
249
250
251
252
# File 'lib/capybara/lightpanda/driver.rb', line 248

def close_window(_handle)
  raise Capybara::NotSupportedByDriverError,
        "Lightpanda has a single window per session; closing it would end the session. " \
        "Use Driver#reset! to start a fresh one."
end

#current_window_handleObject

-- Window Support -- Single-window driver. Lightpanda's BrowserContext is 1:1:1 with Session/Page/target and rejects a second Target.createTarget with TargetAlreadyLoaded (upstream #1962, maintainer-owned), so there is exactly one window and its handle is the CDP target id.

Resizing drives Emulation.setDeviceMetricsOverride, so it is real for window.innerWidth/innerHeight and for what matchMedia reports. Two limits worth knowing before writing a responsive spec:

1. It is not layout. Element geometry stays synthetic, so a resize
 changes which CSS branch applies, never where anything sits.
2. `@media` rules do NOT re-resolve for the document already on
 screen — Lightpanda fixes the cascade at parse time and a metrics
 change doesn't invalidate it. So resize, THEN visit:

   page.current_window.resize_to(375, 667)
   visit "/pricing"   # parses under the new metrics
   assert_selector "#mobile-cta"

 Resizing without re-visiting leaves `matchMedia` and the rendered
 branch disagreeing. See Browser#set_viewport for the verification.


200
201
202
# File 'lib/capybara/lightpanda/driver.rb', line 200

def current_window_handle
  browser.target_id
end

#dismiss_modal(type, **options, &block) ⇒ Object



264
265
266
267
268
# File 'lib/capybara/lightpanda/driver.rb', line 264

def dismiss_modal(type, **options, &block)
  browser.dismiss_modal(type)
  block&.call
  browser.find_modal(type, **{ text: options[:text], wait: options[:wait] }.compact)
end

#downloadsObject

-- Downloads -- Files downloaded since the session started (absolute paths). Capture is on whenever a destination exists (the :save_path driver option, else Capybara.save_path) and the server sends Content-Disposition: attachment. Ferrum/Cuprite expose downloads on the driver too.



126
127
128
# File 'lib/capybara/lightpanda/driver.rb', line 126

def downloads
  browser.downloads.files
end

#evaluate_async_script(script, *args) ⇒ Object



100
101
102
# File 'lib/capybara/lightpanda/driver.rb', line 100

def evaluate_async_script(script, *args)
  unwrap_script_result(browser.evaluate_async(script.strip, *native_args(args)))
end

#evaluate_script(script, *args) ⇒ Object



91
92
93
# File 'lib/capybara/lightpanda/driver.rb', line 91

def evaluate_script(script, *args)
  unwrap_script_result(browser.evaluate(script.strip, *native_args(args)))
end

#execute_script(script, *args) ⇒ Object



95
96
97
98
# File 'lib/capybara/lightpanda/driver.rb', line 95

def execute_script(script, *args)
  browser.execute(script.strip, *native_args(args))
  nil
end

#find_css(selector) ⇒ Object



86
87
88
89
# File 'lib/capybara/lightpanda/driver.rb', line 86

def find_css(selector)
  object_ids = browser.find("css", selector)
  object_ids.map { |oid| Node.new(self, oid) }
end

#find_xpath(selector) ⇒ Object



81
82
83
84
# File 'lib/capybara/lightpanda/driver.rb', line 81

def find_xpath(selector)
  object_ids = browser.find("xpath", selector)
  object_ids.map { |oid| Node.new(self, oid) }
end

#go_backObject



53
54
55
# File 'lib/capybara/lightpanda/driver.rb', line 53

def go_back
  browser.back
end

#go_forwardObject



57
58
59
# File 'lib/capybara/lightpanda/driver.rb', line 57

def go_forward
  browser.forward
end

#headersObject

-- Headers (Cuprite-compatible driver surface) -- Delegates to Network, which lazily enables the Network domain. The overrides do NOT survive reset!: disposing the BrowserContext takes setExtraHTTPHeaders with it, so Network#reset drops its cached copy rather than report headers the browser stopped sending (pinned by network_test.rb "clears extra_headers"). Cuprite exposes these on the driver, and real suites call them there (page.driver.headers = ...).



299
300
301
# File 'lib/capybara/lightpanda/driver.rb', line 299

def headers
  browser.network.extra_headers
end

#headers=(headers) ⇒ Object



303
304
305
# File 'lib/capybara/lightpanda/driver.rb', line 303

def headers=(headers)
  browser.network.headers = headers
end

#htmlObject Also known as: body



65
66
67
# File 'lib/capybara/lightpanda/driver.rb', line 65

def html
  browser.body
end

#invalid_element_errorsObject

Expanded error list for Capybara retry logic (Cuprite pattern). MouseEventFailed is in Cuprite's list, but Lightpanda has no rendering engine and the gem dispatches clicks through JS — the underlying CDP Input.dispatchMouseEvent path doesn't run, so MouseEventFailed is never raised.



350
351
352
353
354
355
356
# File 'lib/capybara/lightpanda/driver.rb', line 350

def invalid_element_errors
  [
    NodeNotFoundError,
    NoExecutionContextError,
    ObsoleteNode,
  ]
end

#maximize_window(handle) ⇒ Object Also known as: fullscreen_window

No window manager and no layout, so "as large as the screen" has no meaning beyond the configured size. Both reset to it rather than raising, because suites call maximize defensively in setup and a raise there would take out the example before it starts.



235
236
237
238
# File 'lib/capybara/lightpanda/driver.rb', line 235

def maximize_window(handle)
  assert_current_window!(handle)
  browser.set_viewport
end

#needs_server?Boolean

Returns:

  • (Boolean)


337
338
339
# File 'lib/capybara/lightpanda/driver.rb', line 337

def needs_server?
  true
end

#networkObject

Network tracker (lazily auto-enabled). Exposes traffic, clear, wait_for_idle, header overrides, etc. Cuprite parity.



108
109
110
# File 'lib/capybara/lightpanda/driver.rb', line 108

def network
  browser.network
end

#no_such_window_errorObject

Capybara's Window#current? rescues this to decide whether a handle is still live, so it must be a class, not a raise.



217
218
219
# File 'lib/capybara/lightpanda/driver.rb', line 217

def no_such_window_error
  NoSuchPageError
end

#open_new_window(_kind = :tab) ⇒ Object

Raises:

  • (Capybara::NotSupportedByDriverError)


242
243
244
245
246
# File 'lib/capybara/lightpanda/driver.rb', line 242

def open_new_window(_kind = :tab)
  raise Capybara::NotSupportedByDriverError,
        "Lightpanda serves a single target per CDP connection (upstream lightpanda-io/browser#1962), " \
        "so a second window cannot be opened. Drive the second page in its own Capybara session instead."
end

#pauseObject

Pause execution for interactive debugging.



359
360
361
362
363
364
365
366
367
368
# File 'lib/capybara/lightpanda/driver.rb', line 359

def pause
  if $stdin.tty?
    warn "\nPaused. Press Enter to continue."
    $stdin.gets
  else
    warn "\nPaused. Send SIGCONT (kill -CONT #{::Process.pid}) to continue."
    trap("CONT") {} # rubocop:disable Lint/EmptyBlock
    ::Process.kill("STOP", ::Process.pid)
  end
end

#quitObject



332
333
334
335
# File 'lib/capybara/lightpanda/driver.rb', line 332

def quit
  @browser&.quit
  @browser = nil
end

#refreshObject



61
62
63
# File 'lib/capybara/lightpanda/driver.rb', line 61

def refresh
  browser.refresh
end


155
156
157
# File 'lib/capybara/lightpanda/driver.rb', line 155

def remove_cookie(name, **)
  browser.cookies.remove(name: name, **)
end

#reset!Object

Thin Cuprite-style wrapper. The interesting work — disposing the BrowserContext (cookies, storage, all targets) and starting a fresh one — happens in Browser#reset.

Rescue is the gem hierarchy plus raw IO escapees only — NOT StandardError: reset! runs between every test, so a blanket rescue turns programmer errors (e.g. a NoMethodError in browser.rb) into a silent quit-and-respawn on every example with zero signal. The warn keeps repeated respawns visible.



322
323
324
325
326
327
328
329
330
# File 'lib/capybara/lightpanda/driver.rb', line 322

def reset!
  browser.reset
rescue Error, SystemCallError, IOError => e
  warn "[capybara-lightpanda] reset! failed (#{e.class}: #{e.message}); respawning browser"
  @browser&.quit
  @browser = nil
ensure
  @started = false
end

#resize_window_to(handle, width, height) ⇒ Object



226
227
228
229
# File 'lib/capybara/lightpanda/driver.rb', line 226

def resize_window_to(handle, width, height)
  assert_current_window!(handle)
  browser.set_viewport(width, height)
end

#save_screenshot(path, **_options) ⇒ Object Also known as: render

-- Screenshots -- Lightpanda has no rendering engine so screenshots are blank, but we handle the call gracefully so Rails' before_teardown (screenshot on failure) doesn't raise NotSupportedByDriverError.



275
276
277
278
279
280
281
282
283
# File 'lib/capybara/lightpanda/driver.rb', line 275

def save_screenshot(path, **_options)
  browser.screenshot(path: path)
rescue BinaryError, BinaryNotFoundError, BrowserError, TimeoutError
  # Browser can't start (version too old), is already dead (DeadBrowserError),
  # the CDP call timed out, or returned any other CDP-level error. Teardown
  # screenshots are best-effort — swallow so the real test failure surfaces
  # instead of a "browser already gone" stack trace.
  nil
end

#send_keys(*keys) ⇒ Object

Capybara's Session#send_keys routes to Driver#send_keys; Cuprite's pattern is to fan that out to whatever element currently has focus.



77
78
79
# File 'lib/capybara/lightpanda/driver.rb', line 77

def send_keys(*keys)
  active_element&.send_keys(*keys)
end

-- Cookie Management --



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/capybara/lightpanda/driver.rb', line 138

def set_cookie(name, value, **options)
  cookie_options = { domain: options[:domain] || default_domain }
  cookie_options[:path] = options[:path] if options[:path]
  cookie_options[:secure] = options[:secure] if options.key?(:secure)
  if options.key?(:httpOnly) || options.key?(:http_only)
    cookie_options[:http_only] =
      options[:httpOnly] || options[:http_only]
  end
  cookie_options[:expires] = options[:expires] if options[:expires]

  browser.cookies.set(name: name, value: value, **cookie_options)
end

#switch_to_frame(frame) ⇒ Object

-- Frame Support -- Passes Node objects (with remote_object_id) to Browser's frame stack. callFunctionOn on the iframe element scopes finding to its contentDocument.



163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/capybara/lightpanda/driver.rb', line 163

def switch_to_frame(frame)
  case frame
  when :top
    browser.clear_frames
  when :parent
    browser.pop_frame
  when Node
    browser.push_frame(frame)
  else
    # Capybara passes a Capybara::Node::Element; extract our driver Node
    browser.push_frame(frame.base)
  end
end

#switch_to_window(handle) ⇒ Object

Raises:



208
209
210
211
212
213
# File 'lib/capybara/lightpanda/driver.rb', line 208

def switch_to_window(handle)
  return if handle == browser.target_id

  raise NoSuchPageError, "Window #{handle.inspect} does not exist. " \
                         "Lightpanda supports a single window per session."
end

#visit(url) ⇒ Object



48
49
50
51
# File 'lib/capybara/lightpanda/driver.rb', line 48

def visit(url)
  @started = true
  browser.go_to(url)
end

#wait?Boolean

Returns:

  • (Boolean)


341
342
343
# File 'lib/capybara/lightpanda/driver.rb', line 341

def wait?
  true
end

#wait_for_download(timeout: 5) ⇒ Object

Block until in-flight downloads finish (or timeout); returns the file list. Click the download trigger first, then call this.



132
133
134
# File 'lib/capybara/lightpanda/driver.rb', line 132

def wait_for_download(timeout: 5)
  browser.downloads.wait(timeout: timeout)
end

#wait_for_network_idle(timeout: 5, connections: 0) ⇒ Object

Block until in-flight HTTP traffic settles. Auto-enables the tracker on first call so callers don't have to remember to flip it on. Returns true on success, false on timeout.



115
116
117
118
# File 'lib/capybara/lightpanda/driver.rb', line 115

def wait_for_network_idle(timeout: 5, connections: 0)
  network.enable
  network.wait_for_idle(timeout: timeout, connections: connections)
end

#window_handlesObject



204
205
206
# File 'lib/capybara/lightpanda/driver.rb', line 204

def window_handles
  [browser.target_id]
end

#window_size(handle) ⇒ Object



221
222
223
224
# File 'lib/capybara/lightpanda/driver.rb', line 221

def window_size(handle)
  assert_current_window!(handle)
  browser.viewport_size
end

#with_lightpanda_browser(&block) ⇒ Object

Escape hatch to the underlying Browser for callers that need raw CDP access — e.g. Lightpanda's LP.* extensions (getMarkdown, getSemanticTree, detectForms, …) that aren't worth exposing through the Capybara DSL. Mirrors capybara-playwright-driver's with_playwright_page. Yields the Browser; returns whatever the block returns.

driver.with_lightpanda_browser do |browser|
browser.page_command("LP.getMarkdown")
end

Raises:

  • (ArgumentError)


42
43
44
45
46
# File 'lib/capybara/lightpanda/driver.rb', line 42

def with_lightpanda_browser(&block)
  raise ArgumentError, "block must be given" unless block

  block.call(browser)
end