Class: Puppeteer::Bidi::Frame

Inherits:
Object
  • Object
show all
Defined in:
lib/puppeteer/bidi/frame.rb,
sig/puppeteer/bidi/frame.rbs

Overview

Frame represents a frame (main frame or iframe) in the page This is a high-level wrapper around Core::BrowsingContext Following Puppeteer's BidiFrame implementation

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Parameters:

Returns:



226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/puppeteer/bidi/frame.rb', line 226

def select(selector, *values)
  assert_not_detached

  handle = query_selector(selector)
  raise SelectorNotFoundError, selector unless handle

  begin
    handle.select(*values)
  ensure
    handle.dispose
  end
end

#set_content(html, wait_until: 'load', timeout: nil) ⇒ void

This method returns an undefined value.

Set frame content

Parameters:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/puppeteer/bidi/frame.rb', line 262

def set_content(html, wait_until: 'load', timeout: nil)
  assert_not_detached

  wait_until_values = wait_until.is_a?(Array) ? wait_until : [wait_until]
  unsupported_value = wait_until_values.find do |value|
    !%w[load domcontentloaded].include?(value)
  end
  raise ArgumentError, "Unknown wait_until value: #{unsupported_value}" if unsupported_value

  events = wait_until_values.uniq.map do |value|
    value == 'load' ? :load : :dom_content_loaded
  end
  listeners = []
  promises = events.map do |event|
    promise = Async::Promise.new
    listener = proc { promise.resolve(nil) }
    @browsing_context.once(event, &listener)
    listeners << [event, listener]
    promise
  end

  timeout_ms = timeout.nil? ? page.timeout_settings.navigation_timeout : timeout
  operation = lambda do
    AsyncUtils.await_promise_all(
      -> { set_frame_content(html) },
      *promises
    )
  end

  if timeout_ms == 0
    operation.call
  else
    AsyncUtils.async_timeout(timeout_ms, operation).wait
  end

  nil
rescue Async::TimeoutError
  raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{timeout_ms} ms exceeded"
ensure
  listeners&.each do |event, listener|
    @browsing_context.off(event, &listener)
  end
end

#set_files(element, files) ⇒ void

This method returns an undefined value.

Set files on an input element

Parameters:



576
577
578
579
580
581
582
583
# File 'lib/puppeteer/bidi/frame.rb', line 576

def set_files(element, files)
  assert_not_detached

  @browsing_context.set_files(
    element.remote_value_as_shared_reference,
    files
  ).wait
end

#set_frame_content(content) ⇒ void

This method returns an undefined value.

Set frame content using document.open/write/close This is a low-level method that doesn't wait for load events

Parameters:



310
311
312
313
314
315
316
317
318
319
320
# File 'lib/puppeteer/bidi/frame.rb', line 310

def set_frame_content(content)
  assert_not_detached

  evaluate(<<~JS, content)
    html => {
      document.open();
      document.write(html);
      document.close();
    }
  JS
end

#type(selector, text, delay: 0) ⇒ void

This method returns an undefined value.

Type text into an element matching the selector

Parameters:



192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/puppeteer/bidi/frame.rb', line 192

def type(selector, text, delay: 0)
  assert_not_detached

  handle = query_selector(selector)
  raise SelectorNotFoundError, selector unless handle

  begin
    handle.type(text, delay: delay)
  ensure
    handle.dispose
  end
end

#urlString

Get the frame URL

Returns:



241
242
243
# File 'lib/puppeteer/bidi/frame.rb', line 241

def url
  @browsing_context.url
end

#wait_for_function(page_function, options = {}, *args, &block) ⇒ void

This method returns an undefined value.

Wait for a function to return a truthy value

Parameters:



556
557
558
# File 'lib/puppeteer/bidi/frame.rb', line 556

def wait_for_function(page_function, options = {}, *args, &block)
  main_realm.wait_for_function(page_function, options, *args, &block)
end

#wait_for_navigation(timeout: nil, wait_until: 'load', &block) ⇒ void

This method returns an undefined value.

Wait for navigation to complete

Parameters:



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
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
# File 'lib/puppeteer/bidi/frame.rb', line 390

def wait_for_navigation(timeout: nil, wait_until: 'load', &block)
  assert_not_detached

  navigation_timeout_ms = timeout || page.timeout_settings.navigation_timeout

  # Normalize wait_until to array
  wait_until_array = wait_until.is_a?(Array) ? wait_until : [wait_until]

  # Separate lifecycle events from network idle events
  lifecycle_events = wait_until_array.select { |e| ['load', 'domcontentloaded'].include?(e) }
  network_idle_events = wait_until_array.select { |e| ['networkidle0', 'networkidle2'].include?(e) }

  # Only wait for lifecycle events if explicitly requested (matches Puppeteer)
  load_event = case lifecycle_events.first
               when 'load'
                 :load
               when 'domcontentloaded'
                 :dom_content_loaded
               else
                 nil
               end

  # Use Async::Promise for signaling (Fiber-based, not Thread-based)
  # This avoids race conditions and follows Puppeteer's Promise-based pattern
  promise = Async::Promise.new

  # Track navigation type for response creation
  navigation_type = nil  # :full_page, :fragment, or :history
  navigation_obj = nil  # The navigation object we're waiting for
  load_listener_registered = false

  # Define load_listener upfront to satisfy type checker
  load_listener = proc do
    promise.resolve(:full_page) unless promise.resolved?
  end

  # Helper to set up navigation listeners
  setup_navigation_listeners = proc do |navigation|
    navigation_obj = navigation
    navigation_type = :full_page

    # Set up listeners for navigation completion
    # Listen for fragment, failed, aborted events
    navigation.once(:fragment) do
      promise.resolve(nil) unless promise.resolved?
    end

    navigation.once(:failed) do
      promise.resolve(nil) unless promise.resolved?
    end

    navigation.once(:aborted) do
      next if detached?
      promise.resolve(nil) unless promise.resolved?
    end

    # Also listen for load/domcontentloaded events to complete navigation
    if load_event
      unless load_listener_registered
        @browsing_context.once(load_event, &load_listener)
        load_listener_registered = true
      end
    else
      # No lifecycle events requested; resolve once navigation is observed.
      promise.resolve(:full_page) unless promise.resolved?
    end
  end

  # Listen for navigation events from BrowsingContext
  # This follows Puppeteer's pattern: race between 'navigation', 'historyUpdated', and 'fragmentNavigated'
  navigation_listener = proc do |navigation|
    # Only handle if we haven't already attached to a navigation
    next if navigation_obj

    setup_navigation_listeners.call(navigation)
  end

  history_listener = proc do
    # History API navigations (without Navigation object)
    # Only resolve if we haven't attached to a navigation
    promise.resolve(nil) unless navigation_obj || promise.resolved?
  end

  fragment_listener = proc do
    # Fragment navigations (anchor links, hash changes)
    # Only resolve if we haven't attached to a navigation
    promise.resolve(nil) unless navigation_obj || promise.resolved?
  end

  closed_listener = proc do
    # Handle frame detachment by rejecting the promise
    promise.reject(FrameDetachedError.new('Navigating frame was detached')) unless promise.resolved?
  end

  @browsing_context.on(:navigation, &navigation_listener)
  @browsing_context.on(:history_updated, &history_listener)
  @browsing_context.on(:fragment_navigated, &fragment_listener)
  @browsing_context.once(:closed, &closed_listener)

  begin
    # CRITICAL: Check for existing navigation BEFORE executing block
    # This follows Puppeteer's pattern where waitForNavigation can attach to
    # an already-started navigation (e.g., when called after goto)
    existing_nav = @browsing_context.navigation
    if existing_nav && !existing_nav.disposed?
      # Attach to the existing navigation
      setup_navigation_listeners.call(existing_nav)
    end

    # Execute the block if provided (this may trigger navigation)
    # Block executes in the same Fiber context for cooperative multitasking
    Async(&block).wait if block

    # Wait for navigation with timeout using Async (Fiber-based)
    if network_idle_events.any?
      # Puppeteer's pattern: wait for both navigation completion AND network idle
      # Determine concurrency based on network idle event
      concurrency = network_idle_events.include?('networkidle0') ? 0 : 2

      # Wait for both navigation and network idle in parallel using promise_all
      if navigation_timeout_ms == 0
        navigation_result, _ = AsyncUtils.await_promise_all(
          promise,
          -> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) }
        )
      else
        navigation_result, _ = AsyncUtils.async_timeout(navigation_timeout_ms, -> do
          AsyncUtils.await_promise_all(
            promise,
            -> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) }
          )
        end).wait
      end

      result = navigation_result
    else
      # Only wait for navigation
      result = if navigation_timeout_ms == 0
                 promise.wait
               else
                 AsyncUtils.async_timeout(navigation_timeout_ms, promise).wait
               end
    end

    # Return HTTPResponse for full page navigation, nil for fragment/history
    return nil unless result == :full_page

    navigation_response_for(navigation_obj)
  rescue Async::TimeoutError
    raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{navigation_timeout_ms} ms exceeded"
  ensure
    # Clean up listeners
    @browsing_context.off(:navigation, &navigation_listener)
    @browsing_context.off(:history_updated, &history_listener)
    @browsing_context.off(:fragment_navigated, &fragment_listener)
    @browsing_context.off(:closed, &closed_listener)
    @browsing_context.off(load_event, &load_listener) if load_listener_registered
  end
end

#wait_for_request_completion(request) ⇒ Object

Parameters:

Returns:



800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
# File 'lib/puppeteer/bidi/frame.rb', line 800

def wait_for_request_completion(request)
  loop do
    return if request.response || request.error

    promise = Async::Promise.new
    success_listener = proc do
      promise.resolve(:done) unless promise.resolved?
    end
    error_listener = proc do
      promise.resolve(:done) unless promise.resolved?
    end
    redirect_listener = proc do |redirect_request|
      promise.resolve(redirect_request) unless promise.resolved?
    end

    request.on(:success, &success_listener)
    request.on(:error, &error_listener)
    request.on(:redirect, &redirect_listener)

    result = promise.wait
    request.off(:success, &success_listener)
    request.off(:error, &error_listener)
    request.off(:redirect, &redirect_listener)

    return if result == :done

    request = result
  end
end

#wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, &block) ⇒ void

This method returns an undefined value.

Wait for an element matching the selector to appear in the frame

Parameters:



567
568
569
570
# File 'lib/puppeteer/bidi/frame.rb', line 567

def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, &block)
  result = QueryHandler.instance.get_query_handler_and_selector(selector)
  result.query_handler.new.wait_for(self, result.updated_selector, visible: visible, hidden: hidden, polling: result.polling, timeout: timeout, &block)
end