Class: Capybara::Lightpanda::Browser

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Browser

Returns a new instance of Browser.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/capybara/lightpanda/browser.rb', line 25

def initialize(options = {})
  @options = Options.new(options)
  @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
  @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_idObject (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

#clientObject (readonly)

Returns the value of attribute client.



10
11
12
# File 'lib/capybara/lightpanda/browser.rb', line 10

def client
  @client
end

#frame_stackObject (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

#optionsObject (readonly)

Returns the value of attribute options.



10
11
12
# File 'lib/capybara/lightpanda/browser.rb', line 10

def options
  @options
end

#processObject (readonly)

Returns the value of attribute process.



10
11
12
# File 'lib/capybara/lightpanda/browser.rb', line 10

def process
  @process
end

#session_idObject (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_idObject (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

Instance Method Details

#accept_modal(_type, text: nil) ⇒ Object



580
581
582
583
584
585
# File 'lib/capybara/lightpanda/browser.rb', line 580

def accept_modal(_type, text: nil)
  prepare_modals
  params = { accept: true }
  params[:promptText] = text if text
  page_command("LP.handleJavaScriptDialog", **params)
end

#active_elementObject

objectId of document.activeElement, or nil if none/document detached.



454
455
456
457
# File 'lib/capybara/lightpanda/browser.rb', line 454

def active_element
  result = evaluate_with_ref("document.activeElement")
  result&.dig("objectId")
end

#backObject



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

def back
  wait_for_navigation { 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.



462
463
464
# File 'lib/capybara/lightpanda/browser.rb', line 462

def backend_node_id(remote_object_id)
  page_command("DOM.describeNode", objectId: remote_object_id).dig("node", "backendNodeId")
end

#bodyObject Also known as: html



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

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.



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/capybara/lightpanda/browser.rb', line 377

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_framesObject



553
554
555
# File 'lib/capybara/lightpanda/browser.rb', line 553

def clear_frames
  @frame_stack.clear
end

#clear_session_stateObject

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.



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/capybara/lightpanda/browser.rb', line 132

def clear_session_state
  @page_events_enabled = false
  @modal_handler_installed = false
  @modal_messages_mutex.synchronize { @modal_messages.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



167
168
169
# File 'lib/capybara/lightpanda/browser.rb', line 167

def command(method, **params)
  @client.command(method, params)
end

#cookiesObject



536
537
538
# File 'lib/capybara/lightpanda/browser.rb', line 536

def cookies
  @cookies ||= Cookies.new(self)
end

#create_browser_contextObject

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.



68
69
70
71
# File 'lib/capybara/lightpanda/browser.rb', line 68

def create_browser_context
  result = @client.command("Target.createBrowserContext")
  @browser_context_id = result["browserContextId"]
end

#create_pageObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/capybara/lightpanda/browser.rb', line 73

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_execution_context
  subscribe_to_turbo_signals
  subscribe_to_navigation_response
  register_auto_scripts
end

#current_urlObject



240
241
242
# File 'lib/capybara/lightpanda/browser.rb', line 240

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.



334
335
336
337
338
# File 'lib/capybara/lightpanda/browser.rb', line 334

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



587
588
589
590
# File 'lib/capybara/lightpanda/browser.rb', line 587

def dismiss_modal(_type)
  prepare_modals
  page_command("LP.handleJavaScriptDialog", accept: false)
end

#enable_page_eventsObject



200
201
202
203
204
205
# File 'lib/capybara/lightpanda/browser.rb', line 200

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.



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/capybara/lightpanda/browser.rb', line 292

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.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/capybara/lightpanda/browser.rb', line 343

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).



362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/capybara/lightpanda/browser.rb', line 362

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).



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/capybara/lightpanda/browser.rb', line 315

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.



416
417
418
419
420
421
422
# File 'lib/capybara/lightpanda/browser.rb', line 416

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



592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/capybara/lightpanda/browser.rb', line 592

def find_modal(type, text: nil, wait: options.timeout)
  regexp = text.is_a?(Regexp) ? text : (text && Regexp.new(Regexp.escape(text.to_s)))
  last_matching_type_message = nil
  last_seen_message = nil
  claimed = nil
  Utils::Wait.until(timeout: wait, interval: 0.05) do
    claimed = pop_modal_message(type.to_s, regexp)
    next true if claimed

    last = peek_last_modal_message(type.to_s)
    last_matching_type_message = last[:matching_type] || last_matching_type_message
    last_seen_message = last[:any] || last_seen_message
    false
  end
  claimed[:message]
rescue TimeoutError
  raise_modal_not_found(type, text, last_matching_type_message, last_seen_message)
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.



433
434
435
436
437
438
439
440
# File 'lib/capybara/lightpanda/browser.rb', line 433

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

#forwardObject



231
232
233
# File 'lib/capybara/lightpanda/browser.rb', line 231

def forward
  wait_for_navigation { navigate_history(+1) }
end

#get_object_properties(remote_object_id) ⇒ Object

Get properties of a remote object (used to extract array elements).



399
400
401
# File 'lib/capybara/lightpanda/browser.rb', line 399

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).



189
190
191
192
193
194
195
196
197
# File 'lib/capybara/lightpanda/browser.rb', line 189

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

#keyboardObject



528
529
530
# File 'lib/capybara/lightpanda/browser.rb', line 528

def keyboard
  @keyboard ||= Keyboard.new(self)
end

#networkObject



532
533
534
# File 'lib/capybara/lightpanda/browser.rb', line 532

def network
  @network ||= Network.new(self)
end

#nightly_buildObject



21
22
23
# File 'lib/capybara/lightpanda/browser.rb', line 21

def nightly_build
  @process&.nightly_build
end

#page_command(method, **params) ⇒ Object



171
172
173
# File 'lib/capybara/lightpanda/browser.rb', line 171

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.



446
447
448
449
450
451
# File 'lib/capybara/lightpanda/browser.rb', line 446

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_frameObject



549
550
551
# File 'lib/capybara/lightpanda/browser.rb', line 549

def pop_frame
  @frame_stack.pop
end

#prepare_modalsObject

– 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.



567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/capybara/lightpanda/browser.rb', line 567

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.



545
546
547
# File 'lib/capybara/lightpanda/browser.rb', line 545

def push_frame(node)
  @frame_stack.push(node)
end

#quitObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/capybara/lightpanda/browser.rb', line 146

def quit
  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

#reconnectObject

Recover after a WebSocket disconnect or process crash during navigation. Restarts the process if it died, then creates a fresh client and page.

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/capybara/lightpanda/browser.rb', line 110

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

#refreshObject Also known as: reload



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

def refresh
  wait_for_navigation { 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.



407
408
409
410
411
412
# File 'lib/capybara/lightpanda/browser.rb', line 407

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

#resetObject

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.

Side benefit: avoids ‘Page.navigate(“about:blank”)` against a non-blank tab, which doesn’t actually replace the document on current Lightpanda nightly (lightpanda-io/browser#2363). The context-disposal path sidesteps that bug entirely.



100
101
102
103
104
105
106
# File 'lib/capybara/lightpanda/browser.rb', line 100

def reset
  dispose_browser_context
  @client.clear_subscriptions
  clear_session_state
  create_browser_context
  create_page
end

#response_headersObject

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.



267
268
269
270
# File 'lib/capybara/lightpanda/browser.rb', line 267

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



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/capybara/lightpanda/browser.rb', line 466

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

#startObject



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/capybara/lightpanda/browser.rb', line 46

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
end

#status_codeObject

HTTP status of the last document navigation; nil before the first navigation completes. Driven by the Network.responseReceived subscription installed in create_page.



260
261
262
# File 'lib/capybara/lightpanda/browser.rb', line 260

def status_code
  @last_navigation_response&.dig(:status)
end

#titleObject



244
245
246
# File 'lib/capybara/lightpanda/browser.rb', line 244

def title
  evaluate("document.title")
end

#versionObject

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.



17
18
19
# File 'lib/capybara/lightpanda/browser.rb', line 17

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.



210
211
212
# File 'lib/capybara/lightpanda/browser.rb', line 210

def wait_for_default_context(timeout = 1.0)
  @default_context_event.wait(timeout)
end

#wait_for_idleObject



513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/capybara/lightpanda/browser.rb', line 513

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.



220
221
222
223
224
225
# File 'lib/capybara/lightpanda/browser.rb', line 220

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