Class: Puppeteer::Bidi::ElementHandle

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

Overview

ElementHandle represents a reference to a DOM element Based on Puppeteer's BidiElementHandle implementation This extends JSHandle with DOM-specific methods

Defined Under Namespace

Classes: BoundingBox, BoxModel, Point

Instance Attribute Summary

Attributes inherited from JSHandle

#realm

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from JSHandle

#as_element, #assert_not_disposed, #dispose, #disposed?, #evaluate, #evaluate_handle, #get_properties, #get_property, #handle_evaluation_exception, #id, #initialize, #json_value, #primitive_value?, #remote_object, #remote_value

Constructor Details

This class inherits a constructor from Puppeteer::Bidi::JSHandle

Class Method Details

.from(remote_value, realm) ⇒ ElementHandle

Factory method to create ElementHandle from remote value

Parameters:

  • remote_value (Hash[String, untyped])
  • realm (Core::Realm)

Returns:



25
26
27
# File 'lib/puppeteer/bidi/element_handle.rb', line 25

def self.from(remote_value, realm)
  new(realm, remote_value)
end

Instance Method Details

#as_locatorLocator

Create a locator based on this element handle.

Returns:



363
364
365
366
367
# File 'lib/puppeteer/bidi/element_handle.rb', line 363

def as_locator
  assert_not_disposed

  NodeLocator.create_from_handle(frame, self)
end

#bounding_boxBoundingBox?

Get the bounding box of the element Uses getBoundingClientRect() to get the element's position and size

Returns:



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
# File 'lib/puppeteer/bidi/element_handle.rb', line 471

def bounding_box
  assert_not_disposed

  result = evaluate(<<~JS)
    element => {
      if (!(element instanceof Element)) {
        return null;
      }
      // Element is not visible
      if (element.getClientRects().length === 0) {
        return null;
      }
      const rect = element.getBoundingClientRect();
      return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
    }
  JS

  return nil unless result

  offset = top_left_corner_of_frame
  return nil unless offset

  BoundingBox.new(
    x: result['x'] + offset[:x],
    y: result['y'] + offset[:y],
    width: result['width'],
    height: result['height']
  )
end

#box_modelBoxModel?

Get the box model of the element (content, padding, border, margin)

Returns:



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
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/puppeteer/bidi/element_handle.rb', line 503

def box_model
  assert_not_disposed

  model = evaluate(<<~JS)
    element => {
      if (!(element instanceof Element)) {
        return null;
      }
      // Element is not visible
      if (element.getClientRects().length === 0) {
        return null;
      }
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      const offsets = {
        padding: {
          left: parseInt(style.paddingLeft, 10),
          top: parseInt(style.paddingTop, 10),
          right: parseInt(style.paddingRight, 10),
          bottom: parseInt(style.paddingBottom, 10),
        },
        margin: {
          left: -parseInt(style.marginLeft, 10),
          top: -parseInt(style.marginTop, 10),
          right: -parseInt(style.marginRight, 10),
          bottom: -parseInt(style.marginBottom, 10),
        },
        border: {
          left: parseInt(style.borderLeftWidth, 10),
          top: parseInt(style.borderTopWidth, 10),
          right: parseInt(style.borderRightWidth, 10),
          bottom: parseInt(style.borderBottomWidth, 10),
        },
      };
      const border = [
        {x: rect.left, y: rect.top},
        {x: rect.left + rect.width, y: rect.top},
        {x: rect.left + rect.width, y: rect.top + rect.height},
        {x: rect.left, y: rect.top + rect.height},
      ];
      const padding = transformQuadWithOffsets(border, offsets.border);
      const content = transformQuadWithOffsets(padding, offsets.padding);
      const margin = transformQuadWithOffsets(border, offsets.margin);
      return {
        content,
        padding,
        border,
        margin,
        width: rect.width,
        height: rect.height,
      };

      function transformQuadWithOffsets(quad, offsets) {
        return [
          {
            x: quad[0].x + offsets.left,
            y: quad[0].y + offsets.top,
          },
          {
            x: quad[1].x - offsets.right,
            y: quad[1].y + offsets.top,
          },
          {
            x: quad[2].x - offsets.right,
            y: quad[2].y - offsets.bottom,
          },
          {
            x: quad[3].x + offsets.left,
            y: quad[3].y - offsets.bottom,
          },
        ];
      }
    }
  JS

  return nil unless model

  offset = top_left_corner_of_frame
  return nil unless offset

  # Convert raw arrays to Point objects for each quad
  BoxModel.new(
    content: model['content'].map do |p|
      Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
    end,
    padding: model['padding'].map do |p|
      Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
    end,
    border: model['border'].map do |p|
      Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
    end,
    margin: model['margin'].map do |p|
      Point.new(x: p['x'] + offset[:x], y: p['y'] + offset[:y])
    end,
    width: model['width'],
    height: model['height']
  )
end

#check_visibility(visible) ⇒ Boolean

Check element visibility

Parameters:

  • visible (Boolean)

Returns:

  • (Boolean)


702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/puppeteer/bidi/element_handle.rb', line 702

def check_visibility(visible)
  assert_not_disposed

  evaluate(<<~JS, visible)
    (node, visible) => {
      const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];

      if (!node) {
        return visible === false;
      }

      // For text nodes, check parent element
      const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
      if (!element) {
        return visible === false;
      }

      const style = window.getComputedStyle(element);
      const rect = element.getBoundingClientRect();
      const isBoundingBoxEmpty = rect.width === 0 || rect.height === 0;

      const isVisible = style &&
        !HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
        !isBoundingBoxEmpty;

      return visible === isVisible;
    }
  JS
end

#click(button: 'left', count: 1, delay: nil, offset: nil) ⇒ void

This method returns an undefined value.

Click the element

Parameters:

  • button: (String) (defaults to: 'left')
  • count: (Integer) (defaults to: 1)
  • delay: (Numeric, nil) (defaults to: nil)
  • offset: (Hash[Symbol, Numeric], nil) (defaults to: nil)


122
123
124
125
126
127
128
129
# File 'lib/puppeteer/bidi/element_handle.rb', line 122

def click(button: 'left', count: 1, delay: nil, offset: nil)
  assert_not_disposed

  scroll_into_view_if_needed
  point = clickable_point(offset: offset)

  frame.page.mouse.click(point.x, point.y, button: button, count: count, delay: delay)
end

#clickable_boxHash[Symbol, Numeric]?

Get the clickable box for the element Uses getClientRects() to handle wrapped/multi-line elements correctly Following Puppeteer's implementation: https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/ElementHandle.ts#clickableBox

Returns:

  • (Hash[Symbol, Numeric], nil)


607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'lib/puppeteer/bidi/element_handle.rb', line 607

def clickable_box
  assert_not_disposed

  # Get client rects - returns multiple boxes for wrapped elements
  boxes = evaluate(<<~JS)
    element => {
      if (!(element instanceof Element)) {
        return null;
      }
      return [...element.getClientRects()].map(rect => {
        return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
      });
    }
  JS

  return nil unless boxes&.is_a?(Array) && !boxes.empty?

  # Intersect boxes with frame boundaries
  intersect_bounding_boxes_with_frame(boxes)

  # TODO: Handle parent frames (for iframe support)
  # frame = self.frame
  # while (parent_frame = frame.parent_frame)
  #   # Adjust coordinates for parent frame offset
  # end

  # Find first box with valid dimensions
  box = boxes.find { |rect| rect['width'] >= 1 && rect['height'] >= 1 }
  return nil unless box

  {
    x: box['x'],
    y: box['y'],
    width: box['width'],
    height: box['height']
  }
end

#clickable_point(offset: nil) ⇒ Point

Get clickable point for the element

Parameters:

  • offset: (Hash[Symbol, Numeric], nil) (defaults to: nil)

Returns:



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/puppeteer/bidi/element_handle.rb', line 449

def clickable_point(offset: nil)
  assert_not_disposed

  box = clickable_box
  raise 'Node is either not clickable or not an Element' unless box

  if offset
    Point.new(
      x: box[:x] + offset[:x],
      y: box[:y] + offset[:y]
    )
  else
    Point.new(
      x: box[:x] + box[:width] / 2,
      y: box[:y] + box[:height] / 2
    )
  end
end

#content_frameFrame?

Get the content frame for iframe/frame elements Returns the frame that the iframe/frame element refers to

Returns:



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/puppeteer/bidi/element_handle.rb', line 174

def content_frame
  assert_not_disposed

  handle = evaluate_handle(<<~JS)
    element => {
      if (element instanceof HTMLIFrameElement || element instanceof HTMLFrameElement) {
        return element.contentWindow;
      }
      return undefined;
    }
  JS

  begin
    value = handle.remote_value
    if value['type'] == 'window'
      # Find the frame with matching browsing context ID
      context_id = value.dig('value', 'context')
      return nil unless context_id

      frame.page.frames.find { |f| f.browsing_context.id == context_id }
    else
      nil
    end
  ensure
    handle.dispose
  end
end

#eval_on_selector(selector, page_function, *args) ⇒ Object

Evaluate a function on the first element matching the selector

Parameters:

  • selector (String)
  • page_function (String)
  • args (Object)

Returns:

  • (Object)


56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/puppeteer/bidi/element_handle.rb', line 56

def eval_on_selector(selector, page_function, *args)
  assert_not_disposed

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

  begin
    element_handle.evaluate(page_function, *args)
  ensure
    element_handle.dispose
  end
end

#eval_on_selector_all(selector, page_function, *args) ⇒ Object

Evaluate a function on all elements matching the selector

Parameters:

  • selector (String)
  • page_function (String)
  • args (Object)

Returns:

  • (Object)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/puppeteer/bidi/element_handle.rb', line 74

def eval_on_selector_all(selector, page_function, *args)
  assert_not_disposed

  # Get all matching elements
  element_handles = query_selector_all(selector)

  begin
    # Create an array handle containing all element handles
    # Use evaluateHandle to create an array in the browser context
    array_handle = @realm.call_function(
      '(...elements) => elements',
      false,
      arguments: element_handles.map(&:remote_value)
    ).wait

    # Create a JSHandle for the array
    array_js_handle = JSHandle.from(array_handle['result'], @realm)

    begin
      # Evaluate the page_function with the array as first argument
      array_js_handle.evaluate(page_function, *args)
    ensure
      array_js_handle.dispose
    end
  ensure
    # Dispose all element handles
    element_handles.each(&:dispose)
  end
end

#focusvoid

This method returns an undefined value.

Focus the element



237
238
239
240
241
# File 'lib/puppeteer/bidi/element_handle.rb', line 237

def focus
  assert_not_disposed

  evaluate('element => element.focus()')
end

#frameFrame

Get the frame this element belongs to Following Puppeteer's pattern: realm.environment

Returns:



167
168
169
# File 'lib/puppeteer/bidi/element_handle.rb', line 167

def frame
  @realm.environment
end

#hidden?Boolean

Check if the element is hidden An element is considered hidden if:

  • It has no computed styles
  • Its visibility is 'hidden' or 'collapse'
  • Its bounding box is empty (width == 0 OR height == 0)

Returns:

  • (Boolean)


218
219
220
# File 'lib/puppeteer/bidi/element_handle.rb', line 218

def hidden?
  check_visibility(false)
end

#hovervoid

This method returns an undefined value.

Hover over the element Scrolls element into view if needed and moves mouse to element center



305
306
307
308
309
310
311
# File 'lib/puppeteer/bidi/element_handle.rb', line 305

def hover
  assert_not_disposed

  scroll_into_view_if_needed
  point = clickable_point
  frame.page.mouse.move(point.x, point.y)
end

#intersect_bounding_box(box, width, height) ⇒ void

This method returns an undefined value.

Intersect a single bounding box with given width/height boundaries Modifies box in-place

Parameters:

  • box (Hash[String, Numeric])
  • width (Numeric)
  • height (Numeric)


677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
# File 'lib/puppeteer/bidi/element_handle.rb', line 677

def intersect_bounding_box(box, width, height)
  # Clip width
  box['width'] = [
    box['x'] >= 0 ?
      [width - box['x'], box['width']].min :
      [width, box['width'] + box['x']].min,
    0
  ].max

  # Clip height
  box['height'] = [
    box['y'] >= 0 ?
      [height - box['y'], box['height']].min :
      [height, box['height'] + box['y']].min,
    0
  ].max

  # Ensure non-negative coordinates
  box['x'] = [box['x'], 0].max
  box['y'] = [box['y'], 0].max
end

#intersect_bounding_boxes_with_frame(boxes) ⇒ void

This method returns an undefined value.

Intersect bounding boxes with frame viewport boundaries Modifies boxes in-place to clip them to visible area

Parameters:

  • boxes (Array[Hash[String, Numeric]])


651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
# File 'lib/puppeteer/bidi/element_handle.rb', line 651

def intersect_bounding_boxes_with_frame(boxes)
  # Get document dimensions using element's evaluate (which handles deserialization)
  dimensions = evaluate(<<~JS)
    element => {
      return {
        documentWidth: element.ownerDocument.documentElement.clientWidth,
        documentHeight: element.ownerDocument.documentElement.clientHeight
      };
    }
  JS

  document_width = dimensions['documentWidth']
  document_height = dimensions['documentHeight']

  # Intersect each box with document boundaries
  boxes.each do |box|
    intersect_bounding_box(box, document_width, document_height)
  end
end

#intersecting_viewport?(threshold: 0) ⇒ Boolean

Check if element is intersecting the viewport

Parameters:

  • threshold: (Numeric) (defaults to: 0)

Returns:

  • (Boolean)


428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/puppeteer/bidi/element_handle.rb', line 428

def intersecting_viewport?(threshold: 0)
  assert_not_disposed

  result = evaluate(<<~JS, threshold)
    (element, threshold) => {
      return new Promise(resolve => {
        const observer = new IntersectionObserver(entries => {
          resolve(entries[0].intersectionRatio > threshold);
          observer.disconnect();
        });
        observer.observe(element);
      });
    }
  JS

  result
end

#non_empty_visible_bounding_boxBoundingBox

Get bounding box ensuring it's non-empty and visible

Returns:



734
735
736
737
738
739
740
741
# File 'lib/puppeteer/bidi/element_handle.rb', line 734

def non_empty_visible_bounding_box
  box = bounding_box
  raise 'Node is either not visible or not an HTMLElement' unless box
  raise 'Node has 0 width.' if box.width.zero?
  raise 'Node has 0 height.' if box.height.zero?

  box
end

#press(key, delay: nil, text: nil) ⇒ void

This method returns an undefined value.

Press a key on the element

Parameters:

  • key (String)
  • delay: (Numeric, nil) (defaults to: nil)
  • text: (String, nil) (defaults to: nil)


152
153
154
155
156
157
158
159
160
161
162
# File 'lib/puppeteer/bidi/element_handle.rb', line 152

def press(key, delay: nil, text: nil)
  assert_not_disposed

  # Focus the element first
  focus

  # Get keyboard instance - use frame.page to access the page
  # Following Puppeteer's pattern: this.frame.page().keyboard
  keyboard = Keyboard.new(frame.page, @realm.browsing_context)
  keyboard.press(key, delay: delay, text: text)
end

#query_selector(selector) ⇒ ElementHandle?

Query for a descendant element matching the selector Supports CSS selectors and prefixed selectors (xpath/, text/, aria/, pierce/)

Parameters:

  • selector (String)

Returns:



33
34
35
36
37
38
# File 'lib/puppeteer/bidi/element_handle.rb', line 33

def query_selector(selector)
  assert_not_disposed

  result = QueryHandler.instance.get_query_handler_and_selector(selector)
  result.query_handler.new.run_query_one(self, result.updated_selector)
end

#query_selector_all(selector) ⇒ Array[ElementHandle]

Query for all descendant elements matching the selector Supports CSS selectors and prefixed selectors (xpath/, text/, aria/, pierce/)

Parameters:

  • selector (String)

Returns:



44
45
46
47
48
49
# File 'lib/puppeteer/bidi/element_handle.rb', line 44

def query_selector_all(selector)
  assert_not_disposed

  result = QueryHandler.instance.get_query_handler_and_selector(selector)
  result.query_handler.new.run_query_all(self, result.updated_selector)
end

#remote_value_as_shared_referenceHash[Symbol, String]

Get the remote value as a SharedReference for BiDi commands

Returns:

  • (Hash[Symbol, String])


334
335
336
337
338
339
340
# File 'lib/puppeteer/bidi/element_handle.rb', line 334

def remote_value_as_shared_reference
  if @remote_value['sharedId']
    { sharedId: @remote_value['sharedId'] }
  else
    { handle: @remote_value['handle'] }
  end
end

#screenshot(path: nil, type: 'png', clip: nil, scroll_into_view: true) ⇒ String

Take a screenshot of the element. Following Puppeteer's implementation: ElementHandle.screenshot

Parameters:

  • path: (String, nil) (defaults to: nil)
  • type: (String) (defaults to: 'png')
  • clip: (Hash[Symbol, Numeric], nil) (defaults to: nil)
  • scroll_into_view: (Boolean) (defaults to: true)

Returns:

  • (String)


376
377
378
379
380
381
382
383
384
385
386
387
388
389
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
# File 'lib/puppeteer/bidi/element_handle.rb', line 376

def screenshot(path: nil, type: 'png', clip: nil, scroll_into_view: true)
  assert_not_disposed

  page = frame.page

  # Scroll into view if needed
  scroll_into_view_if_needed if scroll_into_view

  # Get element's bounding box - must not be empty
  # Note: bounding_box returns viewport-relative coordinates from getBoundingClientRect()
  element_box = non_empty_visible_bounding_box

  # Get page scroll offset from visualViewport to convert to document coordinates
  scroll_offset = evaluate(<<~JS)
    () => {
      if (!window.visualViewport) {
        throw new Error('window.visualViewport is not supported.');
      }
      return {
        pageLeft: window.visualViewport.pageLeft,
        pageTop: window.visualViewport.pageTop
      };
    }
  JS

  # Build element clip in document coordinates (viewport coords + scroll offset)
  # Following Puppeteer's implementation: elementClip.x += pageLeft; elementClip.y += pageTop
  element_clip = {
    x: element_box.x + scroll_offset['pageLeft'],
    y: element_box.y + scroll_offset['pageTop'],
    width: element_box.width,
    height: element_box.height
  }

  # Apply user-specified clip if provided
  if clip
    element_clip[:x] += clip[:x]
    element_clip[:y] += clip[:y]
    element_clip[:width] = clip[:width]
    element_clip[:height] = clip[:height]
  end

  page.screenshot(
    path: path,
    type: type,
    clip: element_clip
  )
end

#scroll_into_viewvoid

This method returns an undefined value.

Scroll element into view



355
356
357
358
359
# File 'lib/puppeteer/bidi/element_handle.rb', line 355

def scroll_into_view
  assert_not_disposed

  evaluate('element => element.scrollIntoView({block: "center", inline: "center", behavior: "instant"})')
end

#scroll_into_view_if_neededvoid

This method returns an undefined value.

Scroll element into view if needed



344
345
346
347
348
349
350
351
# File 'lib/puppeteer/bidi/element_handle.rb', line 344

def scroll_into_view_if_needed
  assert_not_disposed

  # Check if element is already visible
  return if intersecting_viewport?(threshold: 1)

  scroll_into_view
end

#select(*values) ⇒ Array[String]

Select options on a ) Following Puppeteer's implementation: ElementHandle.uploadFile -> Frame.setFiles

Parameters:

  • files (String)


317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/puppeteer/bidi/element_handle.rb', line 317

def upload_file(*files)
  assert_not_disposed

  # Resolve relative paths to absolute paths
  files = files.map do |file|
    if File.absolute_path?(file)
      file
    else
      File.expand_path(file)
    end
  end

  frame.set_files(self, files)
end

#visible?Boolean

Check if the element is visible An element is considered visible if:

  • It has computed styles
  • Its visibility is not 'hidden' or 'collapse'
  • Its bounding box is not empty (width > 0 AND height > 0)

Returns:

  • (Boolean)


208
209
210
# File 'lib/puppeteer/bidi/element_handle.rb', line 208

def visible?
  check_visibility(true)
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 as a descendant of this element

Parameters:

  • selector (String)
  • visible: (Boolean, nil) (defaults to: nil)
  • hidden: (Boolean, nil) (defaults to: nil)
  • timeout: (Numeric, nil) (defaults to: nil)


111
112
113
114
# File 'lib/puppeteer/bidi/element_handle.rb', line 111

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