Class: Capybara::Lightpanda::Browser
- Inherits:
-
Object
- Object
- Capybara::Lightpanda::Browser
- Extended by:
- Forwardable
- Includes:
- Console, Finder, Modals, Navigation, Runtime
- Defined in:
- lib/capybara/lightpanda/browser.rb,
lib/capybara/lightpanda/browser/finder.rb,
lib/capybara/lightpanda/browser/modals.rb,
lib/capybara/lightpanda/browser/console.rb,
lib/capybara/lightpanda/browser/runtime.rb,
lib/capybara/lightpanda/browser/navigation.rb
Defined Under Namespace
Modules: Console, Finder, Modals, Navigation, Runtime
Constant Summary collapse
- NODE_MARKER =
Sentinel key marking a serialized DOM node in JS-result payloads. Produced by #unwrap_call_result / #serialize_remote_array, consumed by Driver#unwrap_script_result, which wraps the objectId in a Node.
"__lightpanda_node__"
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
-
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
-
#alive? ⇒ Boolean
Liveness of the CDP transport.
-
#backend_node_id(remote_object_id) ⇒ Object
Resolve an objectId to its stable per-page backendNodeId.
- #body ⇒ Object (also: #html)
- #clear_frames ⇒ Object
- #command(method, **params) ⇒ Object
- #cookies ⇒ Object
- #current_url ⇒ Object
- #frame_title ⇒ Object
-
#frame_url ⇒ Object
Capybara::Driver::Base resolves frame_url/frame_title via the top execution context, which always reports the parent document.
-
#initialize(options = {}) ⇒ Browser
constructor
A new instance of Browser.
- #keyboard ⇒ Object
- #network ⇒ Object
- #nightly_build ⇒ Object
- #page_command(method, **params) ⇒ Object
- #pop_frame ⇒ Object
-
#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.
-
#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.
Methods included from Console
#clear_console_logs, #console_logs
Methods included from Modals
#accept_modal, #dismiss_modal, #find_modal
Methods included from Navigation
#back, #forward, #go_to, #refresh
Methods included from Finder
#find, #find_within, #parents_of
Methods included from Runtime
#call_function_on, #evaluate, #evaluate_async, #evaluate_with_ref, #execute, #release_object
Constructor Details
#initialize(options = {}) ⇒ Browser
Returns a new instance of Browser.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/capybara/lightpanda/browser.rb', line 85 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 start end |
Instance Attribute Details
#browser_context_id ⇒ Object (readonly)
Returns the value of attribute browser_context_id.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def browser_context_id @browser_context_id end |
#client ⇒ Object (readonly)
Returns the value of attribute client.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def client @client end |
#frame_stack ⇒ Object (readonly)
Returns the value of attribute frame_stack.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def frame_stack @frame_stack end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def @options end |
#process ⇒ Object (readonly)
Returns the value of attribute process.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def process @process end |
#session_id ⇒ Object (readonly)
Returns the value of attribute session_id.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 def session_id @session_id end |
#target_id ⇒ Object (readonly)
Returns the value of attribute target_id.
22 23 24 |
# File 'lib/capybara/lightpanda/browser.rb', line 22 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.
65 66 67 68 69 70 71 |
# File 'lib/capybara/lightpanda/browser.rb', line 65 def quit_all @live_mutex.synchronize { @live.dup }.each do |browser| browser.quit rescue StandardError nil end end |
.track(browser) ⇒ Object
48 49 50 51 52 53 54 55 56 |
# File 'lib/capybara/lightpanda/browser.rb', line 48 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
58 59 60 |
# File 'lib/capybara/lightpanda/browser.rb', line 58 def untrack(browser) @live_mutex.synchronize { @live.delete(browser) } end |
Instance Method Details
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
311 312 313 314 |
# File 'lib/capybara/lightpanda/browser.rb', line 311 def active_element result = evaluate_with_ref("document.activeElement") result&.dig("objectId") end |
#alive? ⇒ Boolean
Liveness of the CDP transport. Driver#browser checks this to decide whether to respawn a dead browser.
208 209 210 211 212 |
# File 'lib/capybara/lightpanda/browser.rb', line 208 def alive? !client.nil? && !client.closed? rescue StandardError false 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.
319 320 321 |
# File 'lib/capybara/lightpanda/browser.rb', line 319 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
286 287 288 289 290 291 292 |
# File 'lib/capybara/lightpanda/browser.rb', line 286 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 |
#clear_frames ⇒ Object
421 422 423 |
# File 'lib/capybara/lightpanda/browser.rb', line 421 def clear_frames @frame_stack.clear end |
#command(method, **params) ⇒ Object
243 244 245 |
# File 'lib/capybara/lightpanda/browser.rb', line 243 def command(method, **params) @client.command(method, params) end |
#cookies ⇒ Object
404 405 406 |
# File 'lib/capybara/lightpanda/browser.rb', line 404 def @cookies ||= Cookies.new(self) end |
#current_url ⇒ Object
278 279 280 |
# File 'lib/capybara/lightpanda/browser.rb', line 278 def current_url evaluate("window.location.href") end |
#frame_title ⇒ Object
436 437 438 439 440 441 |
# File 'lib/capybara/lightpanda/browser.rb', line 436 def frame_title frame = frame_stack.last return title unless frame call_function_on(frame.remote_object_id, FRAME_TITLE_JS) end |
#frame_url ⇒ Object
Capybara::Driver::Base resolves frame_url/frame_title via the top execution context, which always reports the parent document. Resolve them through the iframe element’s contentWindow / contentDocument so they reflect the active frame.
429 430 431 432 433 434 |
# File 'lib/capybara/lightpanda/browser.rb', line 429 def frame_url frame = frame_stack.last return current_url unless frame call_function_on(frame.remote_object_id, FRAME_URL_JS) end |
#keyboard ⇒ Object
396 397 398 |
# File 'lib/capybara/lightpanda/browser.rb', line 396 def keyboard @keyboard ||= Keyboard.new(self) end |
#network ⇒ Object
400 401 402 |
# File 'lib/capybara/lightpanda/browser.rb', line 400 def network @network ||= Network.new(self) end |
#nightly_build ⇒ Object
81 82 83 |
# File 'lib/capybara/lightpanda/browser.rb', line 81 def nightly_build @process&.nightly_build end |
#page_command(method, **params) ⇒ Object
247 248 249 |
# File 'lib/capybara/lightpanda/browser.rb', line 247 def page_command(method, **params) @client.command(method, params, session_id: @session_id) end |
#pop_frame ⇒ Object
417 418 419 |
# File 'lib/capybara/lightpanda/browser.rb', line 417 def pop_frame @frame_stack.pop 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.
413 414 415 |
# File 'lib/capybara/lightpanda/browser.rb', line 413 def push_frame(node) @frame_stack.push(node) end |
#quit ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/capybara/lightpanda/browser.rb', line 214 def quit self.class.untrack(self) # Flip Network back to disabled so a later #start re-installs its # subscriptions — without this, quit→start reuse of the same # instance leaves @enabled true and create_page's network.enable # no-ops, silently killing status_code/traffic capture. Guarded on # @client: with no client the handlers are already moot and # unsubscribe would have nothing to detach from. @network&.reset if @client 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.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/capybara/lightpanda/browser.rb', line 171 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 |
#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.
161 162 163 164 165 166 167 |
# File 'lib/capybara/lightpanda/browser.rb', line 161 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.
305 306 307 308 |
# File 'lib/capybara/lightpanda/browser.rb', line 305 def response_headers raw = network.&.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
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/capybara/lightpanda/browser.rb', line 334 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).
330 331 332 |
# File 'lib/capybara/lightpanda/browser.rb', line 330 def set_file_input_files(remote_object_id, paths) page_command("DOM.setFileInputFiles", objectId: remote_object_id, files: paths) end |
#start ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/capybara/lightpanda/browser.rb', line 106 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. Captured by Network’s subscription (installed via network.enable in create_page).
298 299 300 |
# File 'lib/capybara/lightpanda/browser.rb', line 298 def status_code network.&.dig(:status) end |
#title ⇒ Object
282 283 284 |
# File 'lib/capybara/lightpanda/browser.rb', line 282 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.
77 78 79 |
# File 'lib/capybara/lightpanda/browser.rb', line 77 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.
261 262 263 |
# File 'lib/capybara/lightpanda/browser.rb', line 261 def wait_for_default_context(timeout = 1.0) @default_context_event.wait(timeout) end |
#wait_for_idle ⇒ Object
381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
# File 'lib/capybara/lightpanda/browser.rb', line 381 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.
271 272 273 274 275 276 |
# File 'lib/capybara/lightpanda/browser.rb', line 271 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 |