Class: Capybara::Lightpanda::Browser
- Inherits:
-
Object
- Object
- Capybara::Lightpanda::Browser
- Extended by:
- Forwardable
- Defined in:
- lib/capybara/lightpanda/browser.rb
Instance Attribute Summary collapse
-
#browser_context_id ⇒ Object
readonly
Returns the value of attribute browser_context_id.
-
#client ⇒ Object
readonly
Returns the value of attribute client.
-
#frame_stack ⇒ Object
readonly
Returns the value of attribute frame_stack.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#process ⇒ Object
readonly
Returns the value of attribute process.
-
#session_id ⇒ Object
readonly
Returns the value of attribute session_id.
-
#target_id ⇒ Object
readonly
Returns the value of attribute target_id.
Class Method Summary collapse
-
.quit_all ⇒ Object
at_exit handler: close every live browser’s CDP WebSocket (via #quit) before its Process finalizer can SIGTERM the binary.
- .track(browser) ⇒ Object
- .untrack(browser) ⇒ Object
Instance Method Summary collapse
- #accept_modal(_type, text: nil) ⇒ Object
-
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
- #back ⇒ Object
-
#backend_node_id(remote_object_id) ⇒ Object
Resolve an objectId to its stable per-page backendNodeId.
- #body ⇒ Object (also: #html)
-
#call_function_on(remote_object_id, function_declaration, *args, return_by_value: true) ⇒ Object
Call a function on a remote object via Runtime.callFunctionOn.
- #clear_console_logs ⇒ Object
- #clear_frames ⇒ Object
-
#clear_session_state ⇒ Object
Per-session in-memory state that must be wiped whenever the underlying CDP connection is replaced (#reset disposes the BrowserContext, #reconnect builds a fresh Client).
- #command(method, **params) ⇒ Object
-
#console_logs ⇒ Object
Console messages captured from ‘Runtime.consoleAPICalled` since the last `reset` (Turbo-tracker sentinels excluded).
- #cookies ⇒ Object
-
#create_browser_context ⇒ Object
Per-session BrowserContext (Chrome’s incognito-profile primitive).
- #create_page ⇒ Object
- #current_url ⇒ Object
-
#debug_js_failure(site, expression, response) ⇒ Object
When LIGHTPANDA_DEBUG=1 is set, log the JS expression and full CDP response for every JsException to STDERR.
- #dismiss_modal(_type) ⇒ Object
- #enable_page_events ⇒ Object
-
#evaluate(expression, *args) ⇒ Object
Evaluate JS and return a serialized value.
-
#evaluate_async(expression, *args, wait: @options.timeout) ⇒ Object
Evaluate async JS with a callback.
-
#evaluate_with_ref(expression) ⇒ Object
Evaluate JS and return a RemoteObject reference (for DOM nodes, arrays).
-
#execute(expression, *args) ⇒ Object
Execute JS without returning a value.
-
#find(method, selector) ⇒ Object
Find elements in the current context (top frame or active frame).
-
#find_modal(type, text: nil, wait: options.timeout) ⇒ Object
‘type` is accepted for the error message only: like Selenium (where alert/confirm are indistinguishable) and Cuprite (whose dialog handler accepts whatever fires), we deliberately do NOT reject a dialog whose reported type differs from the one Capybara asked for.
-
#find_within(remote_object_id, method, selector) ⇒ Object
Find child elements within a specific node.
- #forward ⇒ Object
-
#get_object_properties(remote_object_id) ⇒ Object
Get properties of a remote object (used to extract array elements).
-
#go_to(url, wait: true, retried: false) ⇒ Object
(also: #goto)
Navigation with readyState fallback.
-
#initialize(options = {}) ⇒ Browser
constructor
A new instance of Browser.
- #keyboard ⇒ Object
- #network ⇒ Object
- #nightly_build ⇒ Object
- #page_command(method, **params) ⇒ Object
-
#parents_of(remote_object_id) ⇒ Object
Ancestor chain of ‘remote_object_id` from parentNode up to (but excluding) `document`, returned as an array of remote object IDs.
- #pop_frame ⇒ Object
-
#prepare_modals ⇒ Object
– Modal/Dialog Support – Lightpanda’s JS dialogs (alert/confirm/prompt) are driven via the ‘LP.handleJavaScriptDialog` pre-arm model (PR #2261, nightly ≥5900): the client sends `LP.handleJavaScriptDialog promptText` BEFORE the action that triggers the dialog, and the response is consumed when the dialog opens.
-
#push_frame(node) ⇒ Object
– Frame Support – ‘frame_stack` (Array<Node>) is the Capybara `switch_to_frame` stack; it drives where `find` resolves selectors.
- #quit ⇒ Object
-
#reconnect ⇒ Object
Recover after a WebSocket disconnect or process crash during navigation.
- #refresh ⇒ Object (also: #reload)
-
#release_object(remote_object_id) ⇒ Object
Release a remote object reference to free V8 memory.
-
#reset ⇒ Object
Wipe per-session state — cookies, storage, all targets — and start over with a fresh BrowserContext.
-
#response_headers ⇒ Object
Response headers of the last document navigation, wrapped in a Headers instance so ‘[“Content-Type”]` works despite CDP lowercasing keys.
- #screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary) ⇒ Object
-
#set_file_input_files(remote_object_id, paths) ⇒ Object
Populate a file <input> from one or more local file paths via DOM.setFileInputFiles (PR #2635, build ≥6625): Lightpanda resolves the objectId, replaces input.files with a real FileList, and fires ‘input`/`change`.
- #start ⇒ Object
-
#status_code ⇒ Object
HTTP status of the last document navigation; nil before the first navigation completes.
- #title ⇒ Object
-
#version ⇒ Object
Lightpanda binary version (e.g. “lightpanda 0.2.9 nightly.5267”) and parsed nightly build number, captured at Process startup.
-
#wait_for_default_context(timeout = 1.0) ⇒ Object
Block up to ‘timeout` seconds for a default V8 execution context to exist.
- #wait_for_idle ⇒ Object
-
#with_default_context_wait(timeout: 1.0, attempts: 3) ⇒ Object
Run the block; if it raises NoExecutionContextError (the navigation race window — lightpanda-io/browser#2187), wait for the next default context to be signaled by Runtime.executionContextCreated, then retry.
Constructor Details
#initialize(options = {}) ⇒ Browser
Returns a new instance of Browser.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/capybara/lightpanda/browser.rb', line 68 def initialize( = {}) @options = Options.new() @process = nil @client = nil @target_id = nil @session_id = nil @browser_context_id = nil @started = false @page_events_enabled = false @modal_messages = [] @modal_messages_mutex = Mutex.new @modal_handler_installed = false @console_logs = [] @console_logs_mutex = Mutex.new @frame_stack = [] @turbo_event = Utils::Event.new @turbo_event.set @last_navigation_response = nil @document_request_id = nil start end |
Instance Attribute Details
#browser_context_id ⇒ Object (readonly)
Returns the value of attribute browser_context_id.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def browser_context_id @browser_context_id end |
#client ⇒ Object (readonly)
Returns the value of attribute client.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def client @client end |
#frame_stack ⇒ Object (readonly)
Returns the value of attribute frame_stack.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def frame_stack @frame_stack end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def @options end |
#process ⇒ Object (readonly)
Returns the value of attribute process.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def process @process end |
#session_id ⇒ Object (readonly)
Returns the value of attribute session_id.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def session_id @session_id end |
#target_id ⇒ Object (readonly)
Returns the value of attribute target_id.
10 11 12 |
# File 'lib/capybara/lightpanda/browser.rb', line 10 def target_id @target_id end |
Class Method Details
.quit_all ⇒ Object
at_exit handler: close every live browser’s CDP WebSocket (via #quit) before its Process finalizer can SIGTERM the binary. Per-browser rescue so one wedged browser can’t strand the rest.
48 49 50 51 52 53 54 |
# File 'lib/capybara/lightpanda/browser.rb', line 48 def quit_all @live_mutex.synchronize { @live.dup }.each do |browser| browser.quit rescue StandardError nil end end |
.track(browser) ⇒ Object
31 32 33 34 35 36 37 38 39 |
# File 'lib/capybara/lightpanda/browser.rb', line 31 def track(browser) @live_mutex.synchronize do @live << browser unless @live.include?(browser) next if @at_exit_installed @at_exit_installed = true at_exit { quit_all } end end |
.untrack(browser) ⇒ Object
41 42 43 |
# File 'lib/capybara/lightpanda/browser.rb', line 41 def untrack(browser) @live_mutex.synchronize { @live.delete(browser) } end |
Instance Method Details
#accept_modal(_type, text: nil) ⇒ Object
651 652 653 654 655 656 |
# File 'lib/capybara/lightpanda/browser.rb', line 651 def accept_modal(_type, text: nil) prepare_modals params = { accept: true } params[:promptText] = text if text page_command("LP.handleJavaScriptDialog", **params) end |
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
498 499 500 501 |
# File 'lib/capybara/lightpanda/browser.rb', line 498 def active_element result = evaluate_with_ref("document.activeElement") result&.dig("objectId") end |
#back ⇒ Object
271 272 273 |
# File 'lib/capybara/lightpanda/browser.rb', line 271 def back { navigate_history(-1) } end |
#backend_node_id(remote_object_id) ⇒ Object
Resolve an objectId to its stable per-page backendNodeId. objectIds are transient (re-issued per Runtime call) but backendNodeId is stable, so this is what we compare for cross-query node equality.
506 507 508 |
# File 'lib/capybara/lightpanda/browser.rb', line 506 def backend_node_id(remote_object_id) page_command("DOM.describeNode", objectId: remote_object_id).dig("node", "backendNodeId") end |
#body ⇒ Object Also known as: html
292 293 294 295 296 297 298 |
# File 'lib/capybara/lightpanda/browser.rb', line 292 def body # Guard against the brief window after a fresh BrowserContext / target # is created where the V8 context exists but `document.documentElement` # is still null. Hit by Capybara's `#reset_session! resets page body` # spec since the 0.2.0 Ferrum-style reset rewrite. evaluate("(document.documentElement && document.documentElement.outerHTML) || ''") end |
#call_function_on(remote_object_id, function_declaration, *args, return_by_value: true) ⇒ Object
Call a function on a remote object via Runtime.callFunctionOn. Binds ‘this` to the DOM element referenced by remote_object_id.
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
# File 'lib/capybara/lightpanda/browser.rb', line 421 def call_function_on(remote_object_id, function_declaration, *args, return_by_value: true) params = { objectId: remote_object_id, functionDeclaration: function_declaration, returnByValue: return_by_value, awaitPromise: true, } params[:arguments] = args.map { |a| serialize_argument(a) } unless args.empty? response = page_command("Runtime.callFunctionOn", **params) if response["exceptionDetails"] debug_js_failure("call_function_on", function_declaration, response) raise JavaScriptError, response end result = response["result"] return nil if result["type"] == "undefined" return_by_value ? result["value"] : result end |
#clear_console_logs ⇒ Object
607 608 609 |
# File 'lib/capybara/lightpanda/browser.rb', line 607 def clear_console_logs @console_logs_mutex.synchronize { @console_logs.clear } end |
#clear_frames ⇒ Object
624 625 626 |
# File 'lib/capybara/lightpanda/browser.rb', line 624 def clear_frames @frame_stack.clear end |
#clear_session_state ⇒ Object
Per-session in-memory state that must be wiped whenever the underlying CDP connection is replaced (#reset disposes the BrowserContext, #reconnect builds a fresh Client). Without this, a mid-test process crash leaves stale frame_stack Nodes (whose objectIds belong to the dead V8 context) and a ‘@modal_handler_installed = true` flag that makes prepare_modals short-circuit on the new client, so find_modal silently sees no javascriptDialogOpening events.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/capybara/lightpanda/browser.rb', line 174 def clear_session_state @page_events_enabled = false @modal_handler_installed = false @modal_messages_mutex.synchronize { @modal_messages.clear } @console_logs_mutex.synchronize { @console_logs.clear } @last_navigation_response = nil @document_request_id = nil clear_frames # Network#reset, not #clear: disposing the BrowserContext also # destroyed the Network domain and its subscriptions, so we must # flip @enabled back to false — otherwise the next #enable # short-circuits and traffic tracking is silently dead. @network&.reset end |
#command(method, **params) ⇒ Object
211 212 213 |
# File 'lib/capybara/lightpanda/browser.rb', line 211 def command(method, **params) @client.command(method, params) end |
#console_logs ⇒ Object
Console messages captured from ‘Runtime.consoleAPICalled` since the last `reset` (Turbo-tracker sentinels excluded). Loose hashes, like Network#traffic: `text:, timestamp:, args:` where `type` is the console method name (“log”, “error”, “warning”, …), `text` joins the arguments’ primitive values/descriptions, and ‘args` keeps the raw CDP RemoteObjects. Lets suites assert on JS console errors (`browser.console_logs.select { |m| m == “error” }`) the way peer drivers do via custom Ferrum loggers.
603 604 605 |
# File 'lib/capybara/lightpanda/browser.rb', line 603 def console_logs @console_logs_mutex.synchronize { @console_logs.dup } end |
#cookies ⇒ Object
591 592 593 |
# File 'lib/capybara/lightpanda/browser.rb', line 591 def @cookies ||= Cookies.new(self) end |
#create_browser_context ⇒ Object
Per-session BrowserContext (Chrome’s incognito-profile primitive). Cookies, storage, and targets created within the context are wiped when it’s disposed — so ‘reset` is one CDP call instead of an explicit cookies.clear / storage.clear / close-target dance. Mirrors ferrum’s Contexts model.
114 115 116 117 |
# File 'lib/capybara/lightpanda/browser.rb', line 114 def create_browser_context result = @client.command("Target.createBrowserContext") @browser_context_id = result["browserContextId"] end |
#create_page ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/capybara/lightpanda/browser.rb', line 119 def create_page result = @client.command("Target.createTarget", { url: "about:blank", browserContextId: @browser_context_id }.compact) @target_id = result["targetId"] attach_result = @client.command("Target.attachToTarget", { targetId: @target_id, flatten: true }) @session_id = attach_result["sessionId"] @turbo_event.set subscribe_to_console_logs subscribe_to_console_capture subscribe_to_execution_context subscribe_to_turbo_signals register_auto_scripts end |
#current_url ⇒ Object
284 285 286 |
# File 'lib/capybara/lightpanda/browser.rb', line 284 def current_url evaluate("window.location.href") end |
#debug_js_failure(site, expression, response) ⇒ Object
When LIGHTPANDA_DEBUG=1 is set, log the JS expression and full CDP response for every JsException to STDERR. Invaluable for isolating which exact JS triggers an upstream Lightpanda bug.
378 379 380 381 382 |
# File 'lib/capybara/lightpanda/browser.rb', line 378 def debug_js_failure(site, expression, response) return unless ENV["LIGHTPANDA_DEBUG"] warn "[lightpanda:#{site}] expression:\n#{expression}\n[lightpanda:#{site}] response:\n#{response.inspect}\n" end |
#dismiss_modal(_type) ⇒ Object
658 659 660 661 |
# File 'lib/capybara/lightpanda/browser.rb', line 658 def dismiss_modal(_type) prepare_modals page_command("LP.handleJavaScriptDialog", accept: false) end |
#enable_page_events ⇒ Object
244 245 246 247 248 249 |
# File 'lib/capybara/lightpanda/browser.rb', line 244 def enable_page_events return if @page_events_enabled page_command("Page.enable") @page_events_enabled = true end |
#evaluate(expression, *args) ⇒ Object
Evaluate JS and return a serialized value. No-args fast path uses Runtime.evaluate; with args we wrap as a function and dispatch via Runtime.callFunctionOn so ‘arguments` is bound. Both paths use `returnByValue: false` and unwrap so DOM-node returns come back as `{ “lightpanda_node” => … }` for the Driver to wrap.
Even the no-args path wraps the expression in an IIFE to isolate top-level ‘const`/`let` declarations. Upstream Lightpanda retains those bindings across `Runtime.evaluate` calls (V8 starts each call with fresh lexical scope per spec), so a second `const sel = …` raises `SyntaxError: Identifier ’sel’ has already been declared`. Wrapping pushes the declarations into a function scope that gets discarded when the IIFE returns.
Use direct ‘eval` inside the IIFE so the user’s text can be a bare expression (‘’foo’‘), a `throw` statement, OR a multi-statement script with `const`/`let`. `eval`’s completion-value semantics return the last expression’s value in all cases. A naive ‘return EXPR;` wrap would syntax-error on `throw …` and on multi-statement scripts.
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/capybara/lightpanda/browser.rb', line 336 def evaluate(expression, *args) if args.empty? wrapped = "(function(){return eval(#{expression.to_json})})()" response = page_command("Runtime.evaluate", expression: wrapped, returnByValue: false, awaitPromise: true) if response["exceptionDetails"] debug_js_failure("evaluate", expression, response) raise JavaScriptError, response end return unwrap_call_result(response["result"]) end wrapped = "function() { return #{expression} }" call_with_args(wrapped, args) end |
#evaluate_async(expression, *args, wait: @options.timeout) ⇒ Object
Evaluate async JS with a callback. The user’s script receives the callback as its last argument (‘arguments[arguments.length - 1]`), matching Capybara’s evaluate_async_script contract.
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/capybara/lightpanda/browser.rb', line 387 def evaluate_async(expression, *args, wait: @options.timeout) timeout_ms = (wait * 1000).to_i wrapped = <<~JS function() { var __args = Array.prototype.slice.call(arguments); return new Promise(function(__resolve, __reject) { var __timer = setTimeout(function() { __reject(new Error('Async script timeout after #{timeout_ms}ms')); }, #{timeout_ms}); var __done = function(val) { clearTimeout(__timer); __resolve(val); }; __args.push(__done); (function() { #{expression} }).apply(null, __args); }); } JS call_with_args(wrapped, args) end |
#evaluate_with_ref(expression) ⇒ Object
Evaluate JS and return a RemoteObject reference (for DOM nodes, arrays).
406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/capybara/lightpanda/browser.rb', line 406 def evaluate_with_ref(expression) response = page_command("Runtime.evaluate", expression: expression, returnByValue: false, awaitPromise: true) if response["exceptionDetails"] debug_js_failure("evaluate_with_ref", expression, response) raise JavaScriptError, response end result = response["result"] return nil if result["type"] == "undefined" result end |
#execute(expression, *args) ⇒ Object
Execute JS without returning a value.
Like ‘evaluate`, the no-args path wraps in an IIFE — same upstream `const`/`let` leak. Also raises on JS exceptions so silent failures don’t mask test bugs (the previous fast path swallowed them because ‘awaitPromise: false` was checked but `exceptionDetails` was not).
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/capybara/lightpanda/browser.rb', line 359 def execute(expression, *args) if args.empty? wrapped = "(function(){#{expression}})()" response = page_command("Runtime.evaluate", expression: wrapped, returnByValue: false, awaitPromise: false) if response["exceptionDetails"] debug_js_failure("execute", expression, response) raise JavaScriptError, response end return nil end wrapped = "function() { #{expression} }" call_with_args(wrapped, args, return_by_value: false) nil end |
#find(method, selector) ⇒ Object
Find elements in the current context (top frame or active frame). Returns an array of remote object ID strings.
460 461 462 463 464 465 466 |
# File 'lib/capybara/lightpanda/browser.rb', line 460 def find(method, selector) if @frame_stack.empty? find_in_document(method, selector) else find_in_frame(method, selector) end end |
#find_modal(type, text: nil, wait: options.timeout) ⇒ Object
‘type` is accepted for the error message only: like Selenium (where alert/confirm are indistinguishable) and Cuprite (whose dialog handler accepts whatever fires), we deliberately do NOT reject a dialog whose reported type differs from the one Capybara asked for. Real suites wrap `data-confirm` deletes in `accept_alert` (e.g. solidus admin) and expect it to work; only the message text is matched.
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 |
# File 'lib/capybara/lightpanda/browser.rb', line 669 def find_modal(type, text: nil, wait: .timeout) regexp = text.is_a?(Regexp) ? text : (text && Regexp.new(Regexp.escape(text.to_s))) = nil claimed = nil Utils::Wait.until(timeout: wait, interval: 0.05) do claimed = (regexp) next true if claimed = || false end claimed[:message] rescue TimeoutError raise_modal_not_found(type, text, ) end |
#find_within(remote_object_id, method, selector) ⇒ Object
Find child elements within a specific node. Returns an array of remote object ID strings.
Wrapped in ‘with_default_context_wait` so a click that triggered a navigation immediately before the find (e.g. a fill_in following a link that mutated the DOM) doesn’t race against ‘Runtime.executionContextCreated` and surface as `NoExecutionContextError`. `find_in_document` and `find_in_frame` already use the same wrapper; `find_within` was the odd one out.
477 478 479 480 481 482 483 484 |
# File 'lib/capybara/lightpanda/browser.rb', line 477 def find_within(remote_object_id, method, selector) with_default_context_wait do result = call_function_on(remote_object_id, FIND_WITHIN_JS, method, selector, return_by_value: false) extract_node_object_ids(result) end rescue JavaScriptError => e raise_invalid_selector(e, method, selector) end |
#forward ⇒ Object
275 276 277 |
# File 'lib/capybara/lightpanda/browser.rb', line 275 def forward { navigate_history(+1) } end |
#get_object_properties(remote_object_id) ⇒ Object
Get properties of a remote object (used to extract array elements).
443 444 445 |
# File 'lib/capybara/lightpanda/browser.rb', line 443 def get_object_properties(remote_object_id) page_command("Runtime.getProperties", objectId: remote_object_id, ownProperties: true) end |
#go_to(url, wait: true, retried: false) ⇒ Object Also known as: goto
Navigation with readyState fallback.
Lightpanda may never fire Page.loadEventFired on complex JS pages (lightpanda-io/browser#1801, #1832). When the event times out, we poll document.readyState as a fallback.
Page.navigate is sent asynchronously because Lightpanda may not return the command result until the page is fully loaded (unlike Chrome which returns immediately with frameId/loaderId). If we waited synchronously, the readyState fallback would never be reached on pages that fail to fully load.
Uses a single shared deadline so the worst-case wait is 1x timeout, not 2x (lightpanda-io/browser#1849).
233 234 235 236 237 238 239 240 241 |
# File 'lib/capybara/lightpanda/browser.rb', line 233 def go_to(url, wait: true, retried: false) enable_page_events if wait wait_for_page_load(url, retried: retried) else page_command("Page.navigate", url: url) end end |
#keyboard ⇒ Object
583 584 585 |
# File 'lib/capybara/lightpanda/browser.rb', line 583 def keyboard @keyboard ||= Keyboard.new(self) end |
#network ⇒ Object
587 588 589 |
# File 'lib/capybara/lightpanda/browser.rb', line 587 def network @network ||= Network.new(self) end |
#nightly_build ⇒ Object
64 65 66 |
# File 'lib/capybara/lightpanda/browser.rb', line 64 def nightly_build @process&.nightly_build end |
#page_command(method, **params) ⇒ Object
215 216 217 |
# File 'lib/capybara/lightpanda/browser.rb', line 215 def page_command(method, **params) @client.command(method, params, session_id: @session_id) end |
#parents_of(remote_object_id) ⇒ Object
Ancestor chain of ‘remote_object_id` from parentNode up to (but excluding) `document`, returned as an array of remote object IDs. Mirrors Cuprite’s JS ‘parents` helper. Same `with_default_context_wait` wrapping as `find_within` — same race window applies.
490 491 492 493 494 495 |
# File 'lib/capybara/lightpanda/browser.rb', line 490 def parents_of(remote_object_id) with_default_context_wait do result = call_function_on(remote_object_id, PARENTS_JS, return_by_value: false) extract_node_object_ids(result) end end |
#pop_frame ⇒ Object
620 621 622 |
# File 'lib/capybara/lightpanda/browser.rb', line 620 def pop_frame @frame_stack.pop end |
#prepare_modals ⇒ Object
– Modal/Dialog Support – Lightpanda’s JS dialogs (alert/confirm/prompt) are driven via the ‘LP.handleJavaScriptDialog` pre-arm model (PR #2261, nightly ≥5900): the client sends `LP.handleJavaScriptDialog promptText` BEFORE the action that triggers the dialog, and the response is consumed when the dialog opens. `Page.javascriptDialogOpening` still fires, so we capture the message text for `find_modal`. Single-shot: `pending_dialog_response` is one slot, so a second pre-arm before the first dialog opens overwrites the first.
638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'lib/capybara/lightpanda/browser.rb', line 638 def prepare_modals return if @modal_handler_installed enable_page_events on("Page.javascriptDialogOpening") do |params| entry = { type: params["type"], message: params["message"] } @modal_messages_mutex.synchronize { @modal_messages << entry } end @modal_handler_installed = true end |
#push_frame(node) ⇒ Object
– Frame Support – ‘frame_stack` (Array<Node>) is the Capybara `switch_to_frame` stack; it drives where `find` resolves selectors. Stored as Nodes so callFunctionOn can scope to the iframe’s contentDocument.
616 617 618 |
# File 'lib/capybara/lightpanda/browser.rb', line 616 def push_frame(node) @frame_stack.push(node) end |
#quit ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/capybara/lightpanda/browser.rb', line 189 def quit self.class.untrack(self) begin @client&.close rescue StandardError nil end begin @process&.stop rescue StandardError nil end @client = nil @process = nil @started = false @browser_context_id = nil @target_id = nil @session_id = nil @modal_handler_installed = false clear_frames end |
#reconnect ⇒ Object
Recover after a WebSocket disconnect or process crash during navigation. Restarts the process if it died, then creates a fresh client and page.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/capybara/lightpanda/browser.rb', line 152 def reconnect close_client_silently restart_process_if_dead ws_url = @options.ws_url? ? @options.ws_url : @process&.ws_url raise DeadBrowserError, "Cannot reconnect: no WebSocket URL" unless ws_url @client = Client.new(ws_url, @options) # Process may have died; the old browserContextId is gone with it. @browser_context_id = nil clear_session_state create_browser_context create_page end |
#refresh ⇒ Object Also known as: reload
279 280 281 |
# File 'lib/capybara/lightpanda/browser.rb', line 279 def refresh { page_command("Page.reload") } end |
#release_object(remote_object_id) ⇒ Object
Release a remote object reference to free V8 memory. Cleanup is best-effort: callers wrap their work in ‘ensure release_object(…)`, so a TimeoutError or transport hiccup here must not propagate out of the ensure block and bury the original failure.
451 452 453 454 455 456 |
# File 'lib/capybara/lightpanda/browser.rb', line 451 def release_object(remote_object_id) page_command("Runtime.releaseObject", objectId: remote_object_id) rescue Error # Object may already be released, context destroyed, or the CDP call # itself timed out / failed in transport. end |
#reset ⇒ Object
Wipe per-session state — cookies, storage, all targets — and start over with a fresh BrowserContext. Mirrors ferrum’s Browser#reset: one CDP call (‘Target.disposeBrowserContext`) does the work that would otherwise require explicit cookies.clear / storage.clear / close-target dance, and the browser auto-isolates state for the new context. Driver#reset! delegates here.
142 143 144 145 146 147 148 |
# File 'lib/capybara/lightpanda/browser.rb', line 142 def reset dispose_browser_context @client.clear_subscriptions clear_session_state create_browser_context create_page end |
#response_headers ⇒ Object
Response headers of the last document navigation, wrapped in a Headers instance so ‘[“Content-Type”]` works despite CDP lowercasing keys. Returns an empty Headers (not nil) so callers can chain `[]` safely.
311 312 313 314 |
# File 'lib/capybara/lightpanda/browser.rb', line 311 def response_headers raw = @last_navigation_response&.dig(:headers) || {} Headers.new.tap { |h| raw.each { |k, v| h[k.to_s.downcase] = v } } end |
#screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary) ⇒ Object
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 |
# File 'lib/capybara/lightpanda/browser.rb', line 521 def screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary) params = { format: format.to_s } params[:quality] = quality if quality && format == :jpeg if full_page metrics = page_command("Page.getLayoutMetrics") content_size = metrics["contentSize"] params[:clip] = { x: 0, y: 0, width: content_size["width"], height: content_size["height"], scale: 1, } end result = page_command("Page.captureScreenshot", **params) data = result["data"] if encoding == :base64 data else decoded = Base64.decode64(data) if path File.binwrite(path, decoded) path else decoded end end end |
#set_file_input_files(remote_object_id, paths) ⇒ Object
Populate a file <input> from one or more local file paths via DOM.setFileInputFiles (PR #2635, build ≥6625): Lightpanda resolves the objectId, replaces input.files with a real FileList, and fires ‘input`/`change`. The submitted form then carries the bytes as multipart/form-data (PR #2654, build ≥6672) — both halves are needed, which is why MINIMUM_NIGHTLY_BUILD sits at the 6672 floor. Paths are read off the machine running Lightpanda (local for the spawned process).
517 518 519 |
# File 'lib/capybara/lightpanda/browser.rb', line 517 def set_file_input_files(remote_object_id, paths) page_command("DOM.setFileInputFiles", objectId: remote_object_id, files: paths) end |
#start ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/capybara/lightpanda/browser.rb', line 91 def start return if @started if @options.ws_url? @client = Client.new(@options.ws_url, @options) else @process = Process.new(@options) @process.start @client = Client.new(@process.ws_url, @options) end create_browser_context create_page @started = true self.class.track(self) end |
#status_code ⇒ Object
HTTP status of the last document navigation; nil before the first navigation completes. Driven by the Network.responseReceived subscription installed in create_page.
304 305 306 |
# File 'lib/capybara/lightpanda/browser.rb', line 304 def status_code @last_navigation_response&.dig(:status) end |
#title ⇒ Object
288 289 290 |
# File 'lib/capybara/lightpanda/browser.rb', line 288 def title evaluate("document.title") end |
#version ⇒ Object
Lightpanda binary version (e.g. “lightpanda 0.2.9 nightly.5267”) and parsed nightly build number, captured at Process startup. nil when the gem is connecting to an externally-managed Lightpanda via ws_url.
60 61 62 |
# File 'lib/capybara/lightpanda/browser.rb', line 60 def version @process&.version end |
#wait_for_default_context(timeout = 1.0) ⇒ Object
Block up to ‘timeout` seconds for a default V8 execution context to exist. Returns true if available (immediately or after waiting), false if the timeout elapses with no executionContextCreated event.
254 255 256 |
# File 'lib/capybara/lightpanda/browser.rb', line 254 def wait_for_default_context(timeout = 1.0) @default_context_event.wait(timeout) end |
#wait_for_idle ⇒ Object
568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
# File 'lib/capybara/lightpanda/browser.rb', line 568 def wait_for_idle prior_context_iteration = @default_context_event.iteration sniff_deadline = monotonic_time + SNIFF_WINDOW loop do break if @default_context_event.iteration > prior_context_iteration break unless @turbo_event.set? break if monotonic_time > sniff_deadline sleep 0.001 end @default_context_event.wait(@options.timeout) @turbo_event.wait(@options.timeout) end |
#with_default_context_wait(timeout: 1.0, attempts: 3) ⇒ Object
Run the block; if it raises NoExecutionContextError (the navigation race window — lightpanda-io/browser#2187), wait for the next default context to be signaled by Runtime.executionContextCreated, then retry. Up to ‘attempts` total tries; defaults to 3, can be bumped for stubborn flakes. Each retry blocks up to `timeout` seconds for the executionContextCreated signal — no blind sleeps.
264 265 266 267 268 269 |
# File 'lib/capybara/lightpanda/browser.rb', line 264 def with_default_context_wait(timeout: 1.0, attempts: 3) Utils::Attempt.with_retry(errors: NoExecutionContextError, max: attempts, wait: 0) do wait_for_default_context(timeout) yield end end |