Class: Capybara::Lightpanda::Driver
- Inherits:
-
Driver::Base
- Object
- Driver::Base
- Capybara::Lightpanda::Driver
- Extended by:
- Forwardable
- Defined in:
- lib/capybara/lightpanda/driver.rb
Instance Attribute Summary collapse
-
#app ⇒ Object
readonly
Returns the value of attribute app.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
Instance Method Summary collapse
-
#accept_modal(type, **options, &block) ⇒ Object
find_modal owns the wait default (browser.options.timeout) — pass wait only when the caller overrode it.
- #active_element ⇒ Object
- #add_headers(headers) ⇒ Object
- #browser ⇒ Object
- #browser_alive? ⇒ Boolean
- #clear_cookies ⇒ Object
- #close_window(_handle) ⇒ Object
-
#current_window_handle ⇒ Object
-- Window Support -- Single-window driver.
- #dismiss_modal(type, **options, &block) ⇒ Object
-
#downloads ⇒ Object
-- Downloads -- Files downloaded since the session started (absolute paths).
- #evaluate_async_script(script, *args) ⇒ Object
- #evaluate_script(script, *args) ⇒ Object
- #execute_script(script, *args) ⇒ Object
- #find_css(selector) ⇒ Object
- #find_xpath(selector) ⇒ Object
- #go_back ⇒ Object
- #go_forward ⇒ Object
-
#headers ⇒ Object
-- Headers (Cuprite-compatible driver surface) -- Delegates to Network, which lazily enables the Network domain.
- #headers=(headers) ⇒ Object
- #html ⇒ Object (also: #body)
-
#initialize(app, options = {}) ⇒ Driver
constructor
A new instance of Driver.
-
#invalid_element_errors ⇒ Object
Expanded error list for Capybara retry logic (Cuprite pattern).
-
#maximize_window(handle) ⇒ Object
(also: #fullscreen_window)
No window manager and no layout, so "as large as the screen" has no meaning beyond the configured size.
- #needs_server? ⇒ Boolean
-
#network ⇒ Object
Network tracker (lazily auto-enabled).
-
#no_such_window_error ⇒ Object
Capybara's Window#current? rescues this to decide whether a handle is still live, so it must be a class, not a raise.
- #open_new_window(_kind = :tab) ⇒ Object
-
#pause ⇒ Object
Pause execution for interactive debugging.
- #quit ⇒ Object
- #refresh ⇒ Object
- #remove_cookie(name) ⇒ Object
-
#reset! ⇒ Object
Thin Cuprite-style wrapper.
- #resize_window_to(handle, width, height) ⇒ Object
-
#save_screenshot(path, **_options) ⇒ Object
(also: #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.
-
#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.
-
#set_cookie(name, value, **options) ⇒ Object
-- Cookie Management --.
-
#switch_to_frame(frame) ⇒ Object
-- Frame Support -- Passes Node objects (with remote_object_id) to Browser's frame stack.
- #switch_to_window(handle) ⇒ Object
- #visit(url) ⇒ Object
- #wait? ⇒ Boolean
-
#wait_for_download(timeout: 5) ⇒ Object
Block until in-flight downloads finish (or timeout); returns the file list.
-
#wait_for_network_idle(timeout: 5, connections: 0) ⇒ Object
Block until in-flight HTTP traffic settles.
- #window_handles ⇒ Object
- #window_size(handle) ⇒ Object
-
#with_lightpanda_browser(&block) ⇒ Object
Escape hatch to the underlying Browser for callers that need raw CDP access — e.g.
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, = {}) super() @app = app @options = @browser = nil @started = false end |
Instance Attribute Details
#app ⇒ Object (readonly)
Returns the value of attribute app.
11 12 13 |
# File 'lib/capybara/lightpanda/driver.rb', line 11 def app @app end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
11 12 13 |
# File 'lib/capybara/lightpanda/driver.rb', line 11 def @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, **, &block) browser.accept_modal(type, text: [:with]) block&.call browser.find_modal(type, **{ text: [:text], wait: [:wait] }.compact) end |
#active_element ⇒ Object
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 |
#browser ⇒ Object
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
28 29 30 |
# File 'lib/capybara/lightpanda/driver.rb', line 28 def browser_alive? !@browser.nil? && @browser.alive? end |
#clear_cookies ⇒ Object
151 152 153 |
# File 'lib/capybara/lightpanda/driver.rb', line 151 def browser..clear end |
#close_window(_handle) ⇒ Object
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_handle ⇒ Object
-- 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, **, &block) browser.dismiss_modal(type) block&.call browser.find_modal(type, **{ text: [:text], wait: [:wait] }.compact) end |
#downloads ⇒ Object
-- 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_back ⇒ Object
53 54 55 |
# File 'lib/capybara/lightpanda/driver.rb', line 53 def go_back browser.back end |
#go_forward ⇒ Object
57 58 59 |
# File 'lib/capybara/lightpanda/driver.rb', line 57 def go_forward browser.forward end |
#headers ⇒ Object
-- 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 |
#html ⇒ Object Also known as: body
65 66 67 |
# File 'lib/capybara/lightpanda/driver.rb', line 65 def html browser.body end |
#invalid_element_errors ⇒ Object
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. end |
#needs_server? ⇒ Boolean
337 338 339 |
# File 'lib/capybara/lightpanda/driver.rb', line 337 def needs_server? true end |
#network ⇒ Object
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_error ⇒ Object
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
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 |
#pause ⇒ Object
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 |
#quit ⇒ Object
332 333 334 335 |
# File 'lib/capybara/lightpanda/driver.rb', line 332 def quit @browser&.quit @browser = nil end |
#refresh ⇒ Object
61 62 63 |
# File 'lib/capybara/lightpanda/driver.rb', line 61 def refresh browser.refresh end |
#remove_cookie(name) ⇒ Object
155 156 157 |
# File 'lib/capybara/lightpanda/driver.rb', line 155 def (name, **) browser..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.}); 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.(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, **) 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 |
#set_cookie(name, value, **options) ⇒ Object
-- Cookie Management --
138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/capybara/lightpanda/driver.rb', line 138 def (name, value, **) = { domain: [:domain] || default_domain } [:path] = [:path] if [:path] [:secure] = [:secure] if .key?(:secure) if .key?(:httpOnly) || .key?(:http_only) [:http_only] = [:httpOnly] || [:http_only] end [:expires] = [:expires] if [:expires] browser..set(name: name, value: value, **) 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
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
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_handles ⇒ Object
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. 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
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 |