Class: Puppeteer::Bidi::Frame
- Inherits:
-
Object
- Object
- Puppeteer::Bidi::Frame
- 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
-
#browsing_context ⇒ Core::BrowsingContext
readonly
: Core::BrowsingContext.
Class Method Summary collapse
-
.from(parent, browsing_context) ⇒ Frame
Factory method following Puppeteer's BidiFrame.from pattern.
Instance Method Summary collapse
-
#_id ⇒ String
Get the frame ID (browsing context ID) Following Puppeteer's _id pattern.
-
#assert_not_detached ⇒ void
Check if this frame is detached and raise error if so.
-
#child_frames ⇒ Array[Frame]
Get child frames Returns cached frame instances following Puppeteer's pattern.
-
#click(selector, button: 'left', count: 1, delay: nil, offset: nil) ⇒ void
Click an element matching the selector.
- #console_arg_text(remote_value, handle) ⇒ Object
- #console_message_from_log_entry(entry) ⇒ Object
- #console_message_location(stack_trace) ⇒ Object
- #console_message_stack_trace(stack_trace) ⇒ Object
- #console_message_type(method) ⇒ Object
-
#content ⇒ String
Get the full HTML contents of the frame, including the DOCTYPE.
-
#create_frame_target(browsing_context) ⇒ Frame
Create a Frame for a child browsing context Following Puppeteer's BidiFrame.#createFrameTarget pattern exactly: const frame = BidiFrame.from(this, browsingContext); this.#frames.set(browsingContext, frame); this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame); browsingContext.on('closed', () => { this.#frames.delete(browsingContext); }); Note: FrameDetached is NOT emitted here - it's emitted in #initialize when the frame's own browsing context closes.
-
#detached? ⇒ Boolean
Check if frame is detached.
-
#document ⇒ ElementHandle
Get the document element handle.
-
#eval_on_selector(selector, page_function, *args) ⇒ Object
Evaluate a function on the first element matching the selector.
-
#eval_on_selector_all(selector, page_function, *args) ⇒ Object
Evaluate a function on all elements matching the selector.
-
#evaluate(script, *args) ⇒ Object
Evaluate JavaScript in the frame context.
-
#evaluate_handle(script, *args) ⇒ JSHandle
Evaluate JavaScript and return a handle to the result.
-
#expose_function(name, apply = nil, &block) ⇒ void
Expose a Ruby callable as a function in the frame's global context.
-
#frame_element ⇒ ElementHandle?
Get the frame element (iframe/frame DOM element) for this frame Returns nil for the main frame Following Puppeteer's WebDriver BiDi frameElement() implementation.
-
#goto(url, wait_until: 'load', timeout: nil) ⇒ HTTPResponse?
Navigate to a URL.
-
#hover(selector) ⇒ void
Hover over an element matching the selector.
-
#initialize(parent, browsing_context) ⇒ Frame
constructor
A new instance of Frame.
-
#initialize_frame ⇒ void
Initialize the frame by setting up child frame tracking Following Puppeteer's BidiFrame.#initialize pattern exactly.
- #isolated_realm ⇒ FrameRealm
-
#locator(selector = nil, function: nil) ⇒ Locator
Create a locator for a selector or function.
- #main_realm ⇒ FrameRealm
-
#name ⇒ String
Get the frame name.
- #navigation_response_for(navigation) ⇒ Object
-
#page ⇒ Page
Get the page that owns this frame Traverses up the parent chain until reaching a Page.
-
#parent_frame ⇒ Frame?
Get the parent frame.
-
#query_selector(selector) ⇒ ElementHandle?
Query for an element matching the selector.
-
#query_selector_all(selector) ⇒ Array[ElementHandle]
Query for all elements matching the selector.
-
#realm ⇒ FrameRealm
Backwards compatibility for call sites that previously accessed Frame#realm.
-
#remove_exposed_function(name) ⇒ void
Remove an exposed function.
-
#select(selector, *values) ⇒ Array[String]
Select options on a
-
#set_content(html, wait_until: 'load', timeout: nil) ⇒ void
Set frame content.
-
#set_files(element, files) ⇒ void
Set files on an input element.
-
#set_frame_content(content) ⇒ void
Set frame content using document.open/write/close This is a low-level method that doesn't wait for load events.
-
#type(selector, text, delay: 0) ⇒ void
Type text into an element matching the selector.
-
#url ⇒ String
Get the frame URL.
-
#wait_for_function(page_function, options = {}, *args, &block) ⇒ void
Wait for a function to return a truthy value.
-
#wait_for_navigation(timeout: nil, wait_until: 'load', &block) ⇒ void
Wait for navigation to complete.
- #wait_for_request_completion(request) ⇒ Object
-
#wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, &block) ⇒ void
Wait for an element matching the selector to appear in the frame.
Constructor Details
#initialize(parent, browsing_context) ⇒ Frame
Returns a new instance of Frame.
25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/puppeteer/bidi/frame.rb', line 25 def initialize(parent, browsing_context) @parent = parent @browsing_context = browsing_context @frames = {} # Map of browsing context id to Frame (like WeakMap in JS) @exposed_functions = {} # Map of function name to ExposedFunction default_core_realm = @browsing_context.default_realm internal_core_realm = @browsing_context.create_window_realm("__puppeteer_internal_#{rand(1..10_000)}") @main_realm = FrameRealm.new(self, default_core_realm) @isolated_realm = FrameRealm.new(self, internal_core_realm) end |
Instance Attribute Details
#browsing_context ⇒ Core::BrowsingContext (readonly)
: Core::BrowsingContext
10 11 12 |
# File 'lib/puppeteer/bidi/frame.rb', line 10 def browsing_context @browsing_context end |
Class Method Details
.from(parent, browsing_context) ⇒ Frame
Factory method following Puppeteer's BidiFrame.from pattern
16 17 18 19 20 |
# File 'lib/puppeteer/bidi/frame.rb', line 16 def self.from(parent, browsing_context) frame = new(parent, browsing_context) frame.send(:initialize_frame) frame end |
Instance Method Details
#_id ⇒ String
Get the frame ID (browsing context ID) Following Puppeteer's _id pattern
624 625 626 |
# File 'lib/puppeteer/bidi/frame.rb', line 624 def _id @browsing_context.id end |
#assert_not_detached ⇒ void
This method returns an undefined value.
Check if this frame is detached and raise error if so
781 782 783 |
# File 'lib/puppeteer/bidi/frame.rb', line 781 def assert_not_detached raise FrameDetachedError, "Attempted to use detached Frame '#{_id}'." if @browsing_context.closed? end |
#child_frames ⇒ Array[Frame]
Get child frames Returns cached frame instances following Puppeteer's pattern
360 361 362 363 364 |
# File 'lib/puppeteer/bidi/frame.rb', line 360 def child_frames @browsing_context.children.map do |child_context| @frames[child_context.id] end.compact end |
#click(selector, button: 'left', count: 1, delay: nil, offset: nil) ⇒ void
This method returns an undefined value.
Click an element matching the selector
174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/puppeteer/bidi/frame.rb', line 174 def click(selector, button: 'left', count: 1, delay: nil, offset: nil) assert_not_detached handle = query_selector(selector) raise SelectorNotFoundError, selector unless handle begin handle.click(button: , count: count, delay: delay, offset: offset) ensure handle.dispose end end |
#console_arg_text(remote_value, handle) ⇒ Object
723 724 725 726 727 |
# File 'lib/puppeteer/bidi/frame.rb', line 723 def console_arg_text(remote_value, handle) return Deserializer.deserialize(remote_value).to_s if handle.primitive_value? handle.to_s end |
#console_message_from_log_entry(entry) ⇒ Object
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 |
# File 'lib/puppeteer/bidi/frame.rb', line 694 def (entry) args = (entry["args"] || []).map { |arg| JSHandle.from(arg, main_realm.core_realm) } text = args.each_with_index.map do |handle, index| console_arg_text(entry["args"][index], handle) end.join(" ") type = (entry["method"]) ConsoleMessage.new( type: type, text: text, args: args, location: (entry["stackTrace"]), stack_trace: (entry["stackTrace"]) ) end |
#console_message_location(stack_trace) ⇒ Object
729 730 731 732 733 734 735 736 737 738 |
# File 'lib/puppeteer/bidi/frame.rb', line 729 def (stack_trace) frame = stack_trace&.dig("callFrames", 0) return nil unless frame { url: frame["url"], line_number: frame["lineNumber"], column_number: frame["columnNumber"] } end |
#console_message_stack_trace(stack_trace) ⇒ Object
740 741 742 743 744 745 746 747 748 749 |
# File 'lib/puppeteer/bidi/frame.rb', line 740 def (stack_trace) (stack_trace&.fetch("callFrames", nil) || []).map do |frame| { url: frame["url"], line_number: frame["lineNumber"], column_number: frame["columnNumber"], function_name: frame["functionName"] } end end |
#console_message_type(method) ⇒ Object
710 711 712 713 714 715 716 717 718 719 720 721 |
# File 'lib/puppeteer/bidi/frame.rb', line 710 def (method) case method.to_s when "group" "startGroup" when "groupCollapsed" "startGroupCollapsed" when "groupEnd" "endGroup" else method.to_s end end |
#content ⇒ String
Get the full HTML contents of the frame, including the DOCTYPE
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/puppeteer/bidi/frame.rb', line 324 def content assert_not_detached evaluate(<<~JS) () => { let content = ''; for (const node of document.childNodes) { switch (node) { case document.documentElement: content += document.documentElement.outerHTML; break; default: content += new XMLSerializer().serializeToString(node); break; } } return content; } JS end |
#create_frame_target(browsing_context) ⇒ Frame
Create a Frame for a child browsing context Following Puppeteer's BidiFrame.#createFrameTarget pattern exactly:
const frame = BidiFrame.from(this, browsingContext);
this.#frames.set(browsingContext, frame);
this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
browsingContext.on('closed', () => {
this.#frames.delete(browsingContext);
});
Note: FrameDetached is NOT emitted here - it's emitted in #initialize when the frame's own browsing context closes
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 |
# File 'lib/puppeteer/bidi/frame.rb', line 763 def create_frame_target(browsing_context) frame = Frame.from(self, browsing_context) @frames[browsing_context.id] = frame # Emit frameattached event page.emit(:frameattached, frame) # Remove frame from parent's frames map when its context is closed # Note: FrameDetached is emitted by the frame itself in its initialize_frame browsing_context.once(:closed) do @frames.delete(browsing_context.id) end frame end |
#detached? ⇒ Boolean
Check if frame is detached
353 354 355 |
# File 'lib/puppeteer/bidi/frame.rb', line 353 def detached? @browsing_context.closed? end |
#document ⇒ ElementHandle
Get the document element handle
87 88 89 90 91 92 93 94 95 |
# File 'lib/puppeteer/bidi/frame.rb', line 87 def document assert_not_detached handle = main_realm.evaluate_handle('document') unless handle.is_a?(ElementHandle) handle.dispose raise 'Failed to get document' end handle end |
#eval_on_selector(selector, page_function, *args) ⇒ Object
Evaluate a function on the first element matching the selector
144 145 146 147 148 149 150 151 |
# File 'lib/puppeteer/bidi/frame.rb', line 144 def eval_on_selector(selector, page_function, *args) doc = document begin doc.eval_on_selector(selector, page_function, *args) ensure doc.dispose end end |
#eval_on_selector_all(selector, page_function, *args) ⇒ Object
Evaluate a function on all elements matching the selector
158 159 160 161 162 163 164 165 |
# File 'lib/puppeteer/bidi/frame.rb', line 158 def eval_on_selector_all(selector, page_function, *args) doc = document begin doc.eval_on_selector_all(selector, page_function, *args) ensure doc.dispose end end |
#evaluate(script, *args) ⇒ Object
Evaluate JavaScript in the frame context
71 72 73 74 |
# File 'lib/puppeteer/bidi/frame.rb', line 71 def evaluate(script, *args) assert_not_detached main_realm.evaluate(script, *args) end |
#evaluate_handle(script, *args) ⇒ JSHandle
Evaluate JavaScript and return a handle to the result
80 81 82 83 |
# File 'lib/puppeteer/bidi/frame.rb', line 80 def evaluate_handle(script, *args) assert_not_detached main_realm.evaluate_handle(script, *args) end |
#expose_function(name, apply = nil, &block) ⇒ void
This method returns an undefined value.
Expose a Ruby callable as a function in the frame's global context. The function persists across navigations.
591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 |
# File 'lib/puppeteer/bidi/frame.rb', line 591 def expose_function(name, apply = nil, &block) assert_not_detached if @exposed_functions.key?(name) raise Error, "Failed to add page binding with name #{name}: globalThis['#{name}'] already exists!" end handler = apply || block unless handler&.respond_to?(:call) raise ArgumentError, "expose_function requires a callable" end exposed_function = ExposedFunction.from(self, name, handler) @exposed_functions[name] = exposed_function end |
#frame_element ⇒ ElementHandle?
Get the frame element (iframe/frame DOM element) for this frame Returns nil for the main frame Following Puppeteer's WebDriver BiDi frameElement() implementation.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/puppeteer/bidi/frame.rb', line 370 def frame_element assert_not_detached parent = parent_frame return nil unless parent nodes = parent.browsing_context.locate_nodes( { type: 'context', value: { context: @browsing_context.id } } ).wait node = nodes.first return nil unless node ElementHandle.from(node, parent.main_realm.core_realm) end |
#goto(url, wait_until: 'load', timeout: nil) ⇒ HTTPResponse?
Navigate to a URL
250 251 252 253 254 255 |
# File 'lib/puppeteer/bidi/frame.rb', line 250 def goto(url, wait_until: 'load', timeout: nil) response = (timeout: timeout, wait_until: wait_until) do @browsing_context.navigate(url, wait: 'interactive').wait end response end |
#hover(selector) ⇒ void
This method returns an undefined value.
Hover over an element matching the selector
208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/puppeteer/bidi/frame.rb', line 208 def hover(selector) assert_not_detached handle = query_selector(selector) raise SelectorNotFoundError, selector unless handle begin handle.hover ensure handle.dispose end end |
#initialize_frame ⇒ void
This method returns an undefined value.
Initialize the frame by setting up child frame tracking Following Puppeteer's BidiFrame.#initialize pattern exactly
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 |
# File 'lib/puppeteer/bidi/frame.rb', line 633 def initialize_frame # Create Frame objects for existing child contexts @browsing_context.children.each do |child_context| create_frame_target(child_context) end # Listen for new child frames @browsing_context.on(:browsingcontext) do |browsing_context| create_frame_target(browsing_context) end # Emit framedetached when THIS frame's browsing context is closed # Following Puppeteer's pattern: this.browsingContext.on('closed', () => { # this.page().trustedEmitter.emit(PageEvent.FrameDetached, this); # }); @browsing_context.on(:closed) do @frames.clear page.emit(:framedetached, self) end # Listen for navigation events and emit framenavigated # Following Puppeteer's pattern: emit framenavigated on DOMContentLoaded @browsing_context.on(:dom_content_loaded) do page.emit(:framenavigated, self) end # Also emit framenavigated on fragment navigation (anchor links, hash changes) # Note: Puppeteer uses navigation.once('fragment'), but we listen to # browsingContext's fragment_navigated which is equivalent @browsing_context.on(:fragment_navigated) do page.emit(:framenavigated, self) end @browsing_context.on(:request) do |request| http_request = HTTPRequest.from( request, self, page.network_interception_enabled? ) request.once(:success) do page.emit(:requestfinished, http_request) end request.once(:error) do page.emit(:requestfailed, http_request) end page.request_interception_semaphore.async do http_request.finalize_interceptions end end @browsing_context.on(:log) do |entry| if entry["type"] == "console" page.emit(:console, (entry)) elsif entry["type"] == "javascript" page.emit(:pageerror, Error.new(entry["text"].to_s)) end end end |
#isolated_realm ⇒ FrameRealm
44 45 46 |
# File 'lib/puppeteer/bidi/frame.rb', line 44 def isolated_realm @isolated_realm end |
#locator(selector = nil, function: nil) ⇒ Locator
Create a locator for a selector or function.
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/puppeteer/bidi/frame.rb', line 125 def locator(selector = nil, function: nil) assert_not_detached if function raise ArgumentError, "selector and function cannot both be set" if selector FunctionLocator.create(self, function) elsif selector NodeLocator.create(self, selector) else raise ArgumentError, "selector or function must be provided" end end |
#main_realm ⇒ FrameRealm
39 40 41 |
# File 'lib/puppeteer/bidi/frame.rb', line 39 def main_realm @main_realm end |
#name ⇒ String
Get the frame name
347 348 349 |
# File 'lib/puppeteer/bidi/frame.rb', line 347 def name @_name || '' end |
#navigation_response_for(navigation) ⇒ Object
785 786 787 788 789 790 791 792 793 794 795 796 797 798 |
# File 'lib/puppeteer/bidi/frame.rb', line 785 def () return nil unless &.request request = .request resolved_request = request.last_redirect || request http_request = HTTPRequest.for_core_request(resolved_request) return http_request.response if http_request&.response wait_for_request_completion(request) resolved_request = request.last_redirect || request http_request = HTTPRequest.for_core_request(resolved_request) http_request&.response end |
#page ⇒ Page
Get the page that owns this frame Traverses up the parent chain until reaching a Page
57 58 59 |
# File 'lib/puppeteer/bidi/frame.rb', line 57 def page @parent.is_a?(Page) ? @parent : @parent.page end |
#parent_frame ⇒ Frame?
Get the parent frame
63 64 65 |
# File 'lib/puppeteer/bidi/frame.rb', line 63 def parent_frame @parent.is_a?(Frame) ? @parent : nil end |
#query_selector(selector) ⇒ ElementHandle?
Query for an element matching the selector
100 101 102 103 104 105 106 107 |
# File 'lib/puppeteer/bidi/frame.rb', line 100 def query_selector(selector) doc = document begin doc.query_selector(selector) ensure doc.dispose end end |
#query_selector_all(selector) ⇒ Array[ElementHandle]
Query for all elements matching the selector
112 113 114 115 116 117 118 119 |
# File 'lib/puppeteer/bidi/frame.rb', line 112 def query_selector_all(selector) doc = document begin doc.query_selector_all(selector) ensure doc.dispose end end |
#realm ⇒ FrameRealm
Backwards compatibility for call sites that previously accessed Frame#realm.
50 51 52 |
# File 'lib/puppeteer/bidi/frame.rb', line 50 def realm main_realm end |
#remove_exposed_function(name) ⇒ void
This method returns an undefined value.
Remove an exposed function.
610 611 612 613 614 615 616 617 618 619 |
# File 'lib/puppeteer/bidi/frame.rb', line 610 def remove_exposed_function(name) assert_not_detached exposed_function = @exposed_functions.delete(name) unless exposed_function raise Error, "Failed to remove page binding with name #{name}: window['#{name}'] does not exists!" end exposed_function.dispose end |
#select(selector, *values) ⇒ Array[String]
Select options on a
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
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. : 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
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
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
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 |
#url ⇒ String
Get the frame URL
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
556 557 558 |
# File 'lib/puppeteer/bidi/frame.rb', line 556 def wait_for_function(page_function, = {}, *args, &block) main_realm.wait_for_function(page_function, , *args, &block) end |
#wait_for_navigation(timeout: nil, wait_until: 'load', &block) ⇒ void
This method returns an undefined value.
Wait for navigation to complete
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 (timeout: nil, wait_until: 'load', &block) assert_not_detached = timeout || page.timeout_settings. # 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 = nil # :full_page, :fragment, or :history = 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 = proc do || = = :full_page # Set up listeners for navigation completion # Listen for fragment, failed, aborted events .once(:fragment) do promise.resolve(nil) unless promise.resolved? end .once(:failed) do promise.resolve(nil) unless promise.resolved? end .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' = proc do || # Only handle if we haven't already attached to a navigation next if .call() 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 || 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 || 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, &) @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. if existing_nav && !existing_nav.disposed? # Attach to the existing navigation .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 == 0 , _ = AsyncUtils.await_promise_all( promise, -> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) } ) else , _ = AsyncUtils.async_timeout(, -> do AsyncUtils.await_promise_all( promise, -> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) } ) end).wait end result = else # Only wait for navigation result = if == 0 promise.wait else AsyncUtils.async_timeout(, promise).wait end end # Return HTTPResponse for full page navigation, nil for fragment/history return nil unless result == :full_page () rescue Async::TimeoutError raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{} ms exceeded" ensure # Clean up listeners @browsing_context.off(:navigation, &) @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
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
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 |