Class: Capybara::Lightpanda::Browser
- Inherits:
-
Object
- Object
- Capybara::Lightpanda::Browser
- Extended by:
- Forwardable
- Defined in:
- lib/capybara/lightpanda/browser.rb
Instance Attribute Summary collapse
-
#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.
-
#visited_origins ⇒ Object
readonly
Set of ‘scheme://host:port` strings the browser has navigated to during this session.
Instance Method Summary collapse
- #accept_modal(type, text: nil) ⇒ Object
-
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
- #at_css(selector) ⇒ Object
- #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_frames ⇒ Object
- #command(method, **params) ⇒ Object
- #cookies ⇒ Object
- #create_page ⇒ Object
- #css(selector) ⇒ Object
- #current_url ⇒ Object
- #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
-
#find_within(remote_object_id, method, selector) ⇒ Object
Find child elements within a specific node.
- #forward ⇒ Object
- #frame_by(id: nil, name: nil) ⇒ Object
-
#frames ⇒ Object
All frames currently attached to the page (main frame + iframes).
-
#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
-
#main_frame ⇒ Object
The top-level frame, or nil if it hasn’t been registered yet (events arrive asynchronously after Page.enable).
- #network ⇒ Object
- #nightly_build ⇒ Object
- #page_command(method, **params) ⇒ Object
- #pop_frame ⇒ Object
-
#prepare_modals ⇒ Object
– Modal/Dialog Support – Lightpanda auto-dismisses dialogs in headless mode: alert→OK, confirm→false, prompt→null.
-
#push_frame(node) ⇒ Object
– Frame Support – Two parallel views of frames:.
- #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_modals ⇒ Object
- #restart ⇒ Object
- #screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary) ⇒ Object
- #start ⇒ Object
- #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
-
#wait_for_turbo ⇒ Object
Wait for any pending Turbo operations to complete.
-
#with_default_context_wait(timeout: 1.0) ⇒ 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 once.
Constructor Details
#initialize(options = {}) ⇒ Browser
Returns a new instance of Browser.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/capybara/lightpanda/browser.rb', line 27 def initialize( = {}) @options = Options.new() @process = nil @client = nil @target_id = nil @session_id = nil @started = false @page_events_enabled = false @modal_responses = [] @modal_messages = [] @modal_handler_installed = false @frame_stack = [] @frames = Concurrent::Hash.new @turbo_event = Utils::Event.new @turbo_event.set @visited_origins = Concurrent::Set.new start end |
Instance Attribute Details
#client ⇒ Object (readonly)
Returns the value of attribute client.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def client @client end |
#frame_stack ⇒ Object (readonly)
Returns the value of attribute frame_stack.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def frame_stack @frame_stack end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def @options end |
#process ⇒ Object (readonly)
Returns the value of attribute process.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def process @process end |
#session_id ⇒ Object (readonly)
Returns the value of attribute session_id.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def session_id @session_id end |
#target_id ⇒ Object (readonly)
Returns the value of attribute target_id.
12 13 14 |
# File 'lib/capybara/lightpanda/browser.rb', line 12 def target_id @target_id end |
#visited_origins ⇒ Object (readonly)
Set of ‘scheme://host:port` strings the browser has navigated to during this session. Used by Cookies#clear to enumerate cookies across all domains: Lightpanda’s ‘Network.getCookies` (no urls param) is scoped to the current page’s origin, so without tracked origins we’d miss cookies set on previously-visited domains.
52 53 54 |
# File 'lib/capybara/lightpanda/browser.rb', line 52 def visited_origins @visited_origins end |
Instance Method Details
#accept_modal(type, text: nil) ⇒ Object
497 498 499 500 |
# File 'lib/capybara/lightpanda/browser.rb', line 497 def accept_modal(type, text: nil) prepare_modals @modal_responses << { accept: true, text: text, type: type.to_s } end |
#active_element ⇒ Object
objectId of document.activeElement, or nil if none/document detached.
321 322 323 324 |
# File 'lib/capybara/lightpanda/browser.rb', line 321 def active_element result = evaluate_with_ref("document.activeElement") result&.dig("objectId") end |
#at_css(selector) ⇒ Object
338 339 340 341 342 |
# File 'lib/capybara/lightpanda/browser.rb', line 338 def at_css(selector) result = page_command("DOM.querySelector", nodeId: document_node_id, selector: selector) result["nodeId"] end |
#back ⇒ Object
184 185 186 |
# File 'lib/capybara/lightpanda/browser.rb', line 184 def back { execute("history.back()") } 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.
329 330 331 |
# File 'lib/capybara/lightpanda/browser.rb', line 329 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
205 206 207 |
# File 'lib/capybara/lightpanda/browser.rb', line 205 def body evaluate("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.
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/capybara/lightpanda/browser.rb', line 273 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) raise JavaScriptError, response if response["exceptionDetails"] result = response["result"] return nil if result["type"] == "undefined" return_by_value ? result["value"] : result end |
#clear_frames ⇒ Object
451 452 453 |
# File 'lib/capybara/lightpanda/browser.rb', line 451 def clear_frames @frame_stack.clear end |
#command(method, **params) ⇒ Object
123 124 125 |
# File 'lib/capybara/lightpanda/browser.rb', line 123 def command(method, **params) @client.command(method, params) end |
#cookies ⇒ Object
426 427 428 |
# File 'lib/capybara/lightpanda/browser.rb', line 426 def @cookies ||= Cookies.new(self) end |
#create_page ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/capybara/lightpanda/browser.rb', line 70 def create_page result = @client.command("Target.createTarget", { url: "about:blank" }) @target_id = result["targetId"] attach_result = @client.command("Target.attachToTarget", { targetId: @target_id, flatten: true }) @session_id = attach_result["sessionId"] @frames.clear @turbo_event.set subscribe_to_console_logs subscribe_to_execution_context subscribe_to_frame_events subscribe_to_turbo_signals register_auto_scripts end |
#css(selector) ⇒ Object
333 334 335 336 |
# File 'lib/capybara/lightpanda/browser.rb', line 333 def css(selector) node_ids = page_command("DOM.querySelectorAll", nodeId: document_node_id, selector: selector) node_ids["nodeIds"] || [] end |
#current_url ⇒ Object
197 198 199 |
# File 'lib/capybara/lightpanda/browser.rb', line 197 def current_url evaluate("window.location.href") end |
#dismiss_modal(type) ⇒ Object
502 503 504 505 |
# File 'lib/capybara/lightpanda/browser.rb', line 502 def dismiss_modal(type) prepare_modals @modal_responses << { accept: false, type: type.to_s } end |
#enable_page_events ⇒ Object
158 159 160 161 162 163 |
# File 'lib/capybara/lightpanda/browser.rb', line 158 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.
215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/capybara/lightpanda/browser.rb', line 215 def evaluate(expression, *args) if args.empty? response = page_command("Runtime.evaluate", expression: expression, returnByValue: false, awaitPromise: true) raise JavaScriptError, response if response["exceptionDetails"] 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.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/capybara/lightpanda/browser.rb', line 242 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).
261 262 263 264 265 266 267 268 269 |
# File 'lib/capybara/lightpanda/browser.rb', line 261 def evaluate_with_ref(expression) response = page_command("Runtime.evaluate", expression: expression, returnByValue: false, awaitPromise: true) raise JavaScriptError, response if response["exceptionDetails"] result = response["result"] return nil if result["type"] == "undefined" result end |
#execute(expression, *args) ⇒ Object
Execute JS without returning a value.
228 229 230 231 232 233 234 235 236 237 |
# File 'lib/capybara/lightpanda/browser.rb', line 228 def execute(expression, *args) if args.empty? page_command("Runtime.evaluate", expression: expression, returnByValue: false, awaitPromise: false) 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.
305 306 307 308 309 310 311 |
# File 'lib/capybara/lightpanda/browser.rb', line 305 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
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/capybara/lightpanda/browser.rb', line 507 def find_modal(type, text: nil, wait: .timeout) regexp = text.is_a?(Regexp) ? text : (text && Regexp.new(Regexp.escape(text.to_s))) deadline = monotonic_time + wait = nil loop do msg = @modal_messages.find { |m| m[:type] == type.to_s } if msg = msg[:message] if regexp.nil? || .match?(regexp) @modal_messages.delete(msg) return end end break if monotonic_time > deadline sleep 0.05 end raise_modal_not_found(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.
315 316 317 318 |
# File 'lib/capybara/lightpanda/browser.rb', line 315 def find_within(remote_object_id, method, selector) result = call_function_on(remote_object_id, FIND_WITHIN_JS, method, selector, return_by_value: false) extract_node_object_ids(result) end |
#forward ⇒ Object
188 189 190 |
# File 'lib/capybara/lightpanda/browser.rb', line 188 def forward { execute("history.forward()") } end |
#frame_by(id: nil, name: nil) ⇒ Object
466 467 468 469 470 471 472 |
# File 'lib/capybara/lightpanda/browser.rb', line 466 def frame_by(id: nil, name: nil) if id @frames[id] elsif name @frames.each_value.find { |f| f.name == name } end end |
#frames ⇒ Object
All frames currently attached to the page (main frame + iframes).
456 457 458 |
# File 'lib/capybara/lightpanda/browser.rb', line 456 def frames @frames.values end |
#get_object_properties(remote_object_id) ⇒ Object
Get properties of a remote object (used to extract array elements).
292 293 294 |
# File 'lib/capybara/lightpanda/browser.rb', line 292 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).
145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/capybara/lightpanda/browser.rb', line 145 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 record_visited_origin(url) end |
#keyboard ⇒ Object
418 419 420 |
# File 'lib/capybara/lightpanda/browser.rb', line 418 def keyboard @keyboard ||= Keyboard.new(self) end |
#main_frame ⇒ Object
The top-level frame, or nil if it hasn’t been registered yet (events arrive asynchronously after Page.enable).
462 463 464 |
# File 'lib/capybara/lightpanda/browser.rb', line 462 def main_frame @frames.each_value.find(&:main?) end |
#network ⇒ Object
422 423 424 |
# File 'lib/capybara/lightpanda/browser.rb', line 422 def network @network ||= Network.new(self) end |
#nightly_build ⇒ Object
23 24 25 |
# File 'lib/capybara/lightpanda/browser.rb', line 23 def nightly_build @process&.nightly_build end |
#page_command(method, **params) ⇒ Object
127 128 129 |
# File 'lib/capybara/lightpanda/browser.rb', line 127 def page_command(method, **params) @client.command(method, params, session_id: @session_id) end |
#pop_frame ⇒ Object
447 448 449 |
# File 'lib/capybara/lightpanda/browser.rb', line 447 def pop_frame @frame_stack.pop end |
#prepare_modals ⇒ Object
– Modal/Dialog Support – Lightpanda auto-dismisses dialogs in headless mode: alert→OK, confirm→false, prompt→null. Page.javascriptDialogOpening fires (since 2026-04-03), so we capture messages for find_modal, but Page.handleJavaScriptDialog always errors with “No dialog is showing” and we never call it (the dispatch thread cannot make synchronous CDP calls without deadlocking). @modal_responses is retained so accept_modal/dismiss_modal preserve their API contract; the accept/dismiss choice is informational only.
484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'lib/capybara/lightpanda/browser.rb', line 484 def prepare_modals return if @modal_handler_installed enable_page_events on("Page.javascriptDialogOpening") do |params| @modal_messages << { type: params["type"], message: params["message"] } @modal_responses.shift end @modal_handler_installed = true end |
#push_frame(node) ⇒ Object
– Frame Support – Two parallel views of frames:
* `frame_stack` (Array<Node>) — the Capybara `switch_to_frame` stack;
drives where `find` resolves selectors. Stored as Nodes so
callFunctionOn can scope to the iframe's contentDocument.
* `@frames` (Concurrent::Hash<String, Frame>) — metadata view
populated from Page.frame{Attached,Navigated,Detached,...} events.
Used for diagnostics / introspection (frames, main_frame, frame_by).
Lightpanda's frame events are not reliable enough to drive
navigation waits, so this is read-only metadata.
443 444 445 |
# File 'lib/capybara/lightpanda/browser.rb', line 443 def push_frame(node) @frame_stack.push(node) end |
#quit ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/capybara/lightpanda/browser.rb', line 105 def quit begin @client&.close rescue StandardError nil end begin @process&.stop rescue StandardError nil end @client = nil @process = nil @started = false @modal_handler_installed = false @frame_stack.clear 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.
93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/capybara/lightpanda/browser.rb', line 93 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) create_page @page_events_enabled = false end |
#refresh ⇒ Object Also known as: reload
192 193 194 |
# File 'lib/capybara/lightpanda/browser.rb', line 192 def refresh { page_command("Page.reload") } end |
#release_object(remote_object_id) ⇒ Object
Release a remote object reference to free V8 memory.
297 298 299 300 301 |
# File 'lib/capybara/lightpanda/browser.rb', line 297 def release_object(remote_object_id) page_command("Runtime.releaseObject", objectId: remote_object_id) rescue BrowserError, NoExecutionContextError # Object may already be released or context destroyed end |
#reset_modals ⇒ Object
527 528 529 530 |
# File 'lib/capybara/lightpanda/browser.rb', line 527 def reset_modals @modal_responses.clear @modal_messages.clear end |
#restart ⇒ Object
86 87 88 89 |
# File 'lib/capybara/lightpanda/browser.rb', line 86 def restart quit start end |
#screenshot(path: nil, format: :png, quality: nil, full_page: false, encoding: :binary) ⇒ Object
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/capybara/lightpanda/browser.rb', line 344 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 |
#start ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/capybara/lightpanda/browser.rb', line 54 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_page @started = true end |
#title ⇒ Object
201 202 203 |
# File 'lib/capybara/lightpanda/browser.rb', line 201 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.
19 20 21 |
# File 'lib/capybara/lightpanda/browser.rb', line 19 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.
168 169 170 |
# File 'lib/capybara/lightpanda/browser.rb', line 168 def wait_for_default_context(timeout = 1.0) @default_context_event.wait(timeout) end |
#wait_for_idle ⇒ Object
403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/capybara/lightpanda/browser.rb', line 403 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 |
#wait_for_turbo ⇒ Object
Wait for any pending Turbo operations to complete. Event-driven: the injected JS in index.js calls ‘console.debug(’__lightpanda_turbo_busy’)‘ when the pending-ops counter rises above 0 and `_idle` when it returns to 0. We toggle @turbo_event accordingly (see subscribe_to_turbo_signals).
Pages without Turbo never trigger _turboStart, so no sentinels fire and for Turbo-loaded pages that have no pending work.
386 387 388 |
# File 'lib/capybara/lightpanda/browser.rb', line 386 def wait_for_turbo @turbo_event.wait(@options.timeout) end |
#with_default_context_wait(timeout: 1.0) ⇒ 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 once. Replaces blind 100 ms sleep retries.
176 177 178 179 180 181 182 |
# File 'lib/capybara/lightpanda/browser.rb', line 176 def with_default_context_wait(timeout: 1.0) yield rescue NoExecutionContextError raise unless wait_for_default_context(timeout) yield end |