class Capybara::Playwright::Node

Selector and checking methods are derived from twapole/apparition Action methods (click, select_option, …) uses playwright.

ref:

selenium:   https://github.com/teamcapybara/capybara/blob/master/lib/capybara/selenium/node.rb
apparition: https://github.com/twalpole/apparition/blob/master/lib/capybara/apparition/node.rb

Constants

SCROLL_POSITIONS

Public Class Methods

new(driver, page, element) click to toggle source
Calls superclass method
# File lib/capybara/playwright/node.rb, line 67
def initialize(driver, page, element)
  super(driver, element)
  @page = page
  @element = element
end

Public Instance Methods

==(other) click to toggle source
# File lib/capybara/playwright/node.rb, line 898
def ==(other)
  return false unless other.is_a?(Node)

  @element.evaluate('(self, other) => self == other', arg: other.element)
end
[](name) click to toggle source
# File lib/capybara/playwright/node.rb, line 137
def [](name)
  assert_element_not_stale

  property(name) || attribute(name)
end
all_text() click to toggle source
# File lib/capybara/playwright/node.rb, line 104
def all_text
  assert_element_not_stale

  text = @element.text_content
  text.to_s.gsub(/[\u200b\u200e\u200f]/, '')
      .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
      .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
      .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
      .tr("\u00a0", ' ')
end
checked?() click to toggle source
# File lib/capybara/playwright/node.rb, line 812
def checked?
  assert_element_not_stale

  @element.evaluate('el => !!el.checked')
end
click(keys = [], **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 343
def click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  @element.click(**click_options.as_params)
end
disabled?() click to toggle source
# File lib/capybara/playwright/node.rb, line 824
      def disabled?
        @element.evaluate(<<~JAVASCRIPT)
        function(el) {
          const xpath = 'parent::optgroup[@disabled] | \
                        ancestor::select[@disabled] | \
                        parent::fieldset[@disabled] | \
                        ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]';
          return el.disabled || document.evaluate(xpath, el, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
        }
        JAVASCRIPT
      end
double_click(keys = [], **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 355
def double_click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  @element.dblclick(**click_options.as_params)
end
drag_to(element, **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 613
def drag_to(element, **options)
  DragTo.new(@page, @element, element.element, options).execute
end
drop(*args) click to toggle source
# File lib/capybara/playwright/node.rb, line 687
def drop(*args)
  raise NotImplementedError
end
find_css(query, **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 912
def find_css(query, **options)
  assert_element_not_stale

  @element.query_selector_all(query).map do |el|
    Node.new(@driver, @page, el)
  end
end
find_xpath(query, **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 904
def find_xpath(query, **options)
  assert_element_not_stale

  @element.query_selector_all("xpath=#{query}").map do |el|
    Node.new(@driver, @page, el)
  end
end
hover() click to toggle source
# File lib/capybara/playwright/node.rb, line 609
def hover
  @element.hover(timeout: capybara_default_wait_time)
end
inspect() click to toggle source
# File lib/capybara/playwright/node.rb, line 894
def inspect
  %(#<#{self.class} tag="#{tag_name}" path="#{path}">)
end
multiple?() click to toggle source
# File lib/capybara/playwright/node.rb, line 840
def multiple?
  @element.evaluate('el => el.multiple')
end
obscured?() click to toggle source
# File lib/capybara/playwright/node.rb, line 808
def obscured?
  @element.capybara_obscured?
end
path() click to toggle source
# File lib/capybara/playwright/node.rb, line 856
      def path
        assert_element_not_stale

        @element.evaluate(<<~JAVASCRIPT)
        (el) => {
          var xml = document;
          var xpath = '';
          var pos, tempitem2;
          if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
            return "(: Shadow DOM element - no XPath :)";
          };
          while(el !== xml.documentElement) {
            pos = 0;
            tempitem2 = el;
            while(tempitem2) {
              if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
                pos += 1;
              }
              tempitem2 = tempitem2.previousSibling;
            }
            if (el.namespaceURI != xml.documentElement.namespaceURI) {
              xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
            } else {
              xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
            }
            el = el.parentNode;
          }
          xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
          xpath = xpath.replace(/\\/$/, '');
          return xpath;
        }
        JAVASCRIPT
      end
readonly?() click to toggle source
# File lib/capybara/playwright/node.rb, line 836
def readonly?
  !@element.editable?
end
rect() click to toggle source
# File lib/capybara/playwright/node.rb, line 844
      def rect
        assert_element_not_stale

        @element.evaluate(<<~JAVASCRIPT)
        function(el){
          const rects = [...el.getClientRects()]
          const rect = rects.find(r => (r.height && r.width)) || el.getBoundingClientRect();
          return rect.toJSON();
        }
        JAVASCRIPT
      end
right_click(keys = [], **options) click to toggle source
# File lib/capybara/playwright/node.rb, line 348
def right_click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  params = click_options.as_params
  params[:button] = 'right'
  @element.click(**params)
end
scroll_by(x, y) click to toggle source
# File lib/capybara/playwright/node.rb, line 691
      def scroll_by(x, y)
        js = <<~JAVASCRIPT
        (el, [x, y]) => {
          if (el.scrollBy){
            el.scrollBy(x, y);
          } else {
            el.scrollTop = el.scrollTop + y;
            el.scrollLeft = el.scrollLeft + x;
          }
        }
        JAVASCRIPT

        @element.evaluate(js, arg: [x, y])
      end
scroll_to(element, location, position = nil) click to toggle source
# File lib/capybara/playwright/node.rb, line 706
def scroll_to(element, location, position = nil)
  # location, element = element, nil if element.is_a? Symbol
  if element.is_a? Capybara::Playwright::Node
    scroll_element_to_location(element, location)
  elsif location.is_a? Symbol
    scroll_to_location(location)
  else
    scroll_to_coords(*position)
  end

  self
end
select_option() click to toggle source
# File lib/capybara/playwright/node.rb, line 316
def select_option
  return false if disabled?

  select_element = parent_select_element
  if select_element.evaluate('el => el.multiple')
    selected_options = select_element.query_selector_all('option:checked')
    selected_options << @element
    select_element.select_option(element: selected_options, timeout: capybara_default_wait_time)
  else
    select_element.select_option(element: @element, timeout: capybara_default_wait_time)
  end
end
selected?() click to toggle source
# File lib/capybara/playwright/node.rb, line 818
def selected?
  assert_element_not_stale

  @element.evaluate('el => !!el.selected')
end
send_keys(*args) click to toggle source
# File lib/capybara/playwright/node.rb, line 439
def send_keys(*args)
  SendKeys.new(@element, args).execute
end
set(value, **options) click to toggle source

@param value [String, Array] Array is only allowed if node has 'multiple' attribute @param options [Hash] Driver specific options for how to set a value on a node

# File lib/capybara/playwright/node.rb, line 174
def set(value, **options)
  settable_class =
    case tag_name
    when 'input'
      case attribute('type')
      when 'radio'
        RadioButton
      when 'checkbox'
        Checkbox
      when 'file'
        FileUpload
      when 'date'
        DateInput
      when 'time'
        TimeInput
      when 'datetime-local'
        DateTimeInput
      when 'color'
        JSValueInput
      when 'range'
        JSValueInput
      else
        TextInput
      end
    when 'textarea'
      TextInput
    else
      if @element.editable?
        TextInput
      else
        raise NotSupportedByDriverError
      end
    end

  settable_class.new(@element, capybara_default_wait_time).set(value, **options)
rescue ::Playwright::TimeoutError => err
  raise NotActionableError.new(err)
end
style(styles) click to toggle source
# File lib/capybara/playwright/node.rb, line 166
def style(styles)
  # Capybara provides default implementation.
  # ref: https://github.com/teamcapybara/capybara/blob/f7ab0b5cd5da86185816c2d5c30d58145fe654ed/lib/capybara/node/element.rb#L92
  raise NotImplementedError
end
tag_name() click to toggle source
# File lib/capybara/playwright/node.rb, line 770
def tag_name
  @tag_name ||= @element.evaluate('e => e.tagName.toLowerCase()')
end
trigger(event) click to toggle source
# File lib/capybara/playwright/node.rb, line 890
def trigger(event)
  @element.dispatch_event(event)
end
unselect_option() click to toggle source
# File lib/capybara/playwright/node.rb, line 329
def unselect_option
  if parent_select_element.evaluate('el => el.multiple')
    return false if disabled?

    @element.evaluate('el => el.selected = false')
  else
    raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.'
  end
end
value() click to toggle source
# File lib/capybara/playwright/node.rb, line 152
def value
  assert_element_not_stale

  # ref: https://github.com/teamcapybara/capybara/blob/f7ab0b5cd5da86185816c2d5c30d58145fe654ed/lib/capybara/selenium/node.rb#L31
  # ref: https://github.com/twalpole/apparition/blob/11aca464b38b77585191b7e302be2e062bdd369d/lib/capybara/apparition/node.rb#L728
  if tag_name == 'select' && @element.evaluate('el => el.multiple')
    @element.query_selector_all('option:checked').map do |option|
      option.evaluate('el => el.value')
    end
  else
    @element.evaluate('el => el.value')
  end
end
visible?() click to toggle source
# File lib/capybara/playwright/node.rb, line 774
      def visible?
        assert_element_not_stale

        # if an area element, check visibility of relevant image
        @element.evaluate(<<~JAVASCRIPT)
        function(el) {
          if (el.tagName == 'AREA'){
            const map_name = document.evaluate('./ancestor::map/@name', el, null, XPathResult.STRING_TYPE, null).stringValue;
            el = document.querySelector(`img[usemap='#${map_name}']`);
            if (!el){
            return false;
            }
          }
          var forced_visible = false;
          while (el) {
            const style = window.getComputedStyle(el);
            if (style.visibility == 'visible')
              forced_visible = true;
            if ((style.display == 'none') ||
                ((style.visibility == 'hidden') && !forced_visible) ||
                (parseFloat(style.opacity) == 0)) {
              return false;
            }
            var parent = el.parentElement;
            if (parent && (parent.tagName == 'DETAILS') && !parent.open && (el.tagName != 'SUMMARY')) {
              return false;
            }
            el = parent;
          }
          return true;
        }
        JAVASCRIPT
      end
visible_text() click to toggle source
# File lib/capybara/playwright/node.rb, line 115
      def visible_text
        assert_element_not_stale

        return '' unless visible?

        text = @element.evaluate(<<~JAVASCRIPT)
          function(el){
            if (el.nodeName == 'TEXTAREA'){
              return el.textContent;
            } else if (el instanceof SVGElement) {
              return el.textContent;
            } else {
              return el.innerText;
            }
          }
        JAVASCRIPT
        text.to_s.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
            .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
            .gsub(/\n+/, "\n")
            .tr("\u00a0", ' ')
      end

Protected Instance Methods

element() click to toggle source
# File lib/capybara/playwright/node.rb, line 73
          def element
  @element
end

Private Instance Methods

assert_element_not_stale() click to toggle source
# File lib/capybara/playwright/node.rb, line 77
        def assert_element_not_stale
  # Playwright checks the staled state only when
  # actionable methods. (click, select_option, hover, ...)
  # Capybara expects stale checking also when getting inner text, and so on.
  @element.enabled?
rescue ::Playwright::Error => err
  case err.message
  when /Element is not attached to the DOM/
    raise StaleReferenceError.new(err)
  when /Cannot find context with specified id/
    raise StaleReferenceError.new(err)
  when /Unable to adopt element handle from a different document/ # for WebKit.
    raise StaleReferenceError.new(err)
  when /error in channel "content::page": exception while running method "adoptNode"/ # for Firefox
    raise StaleReferenceError.new(err)
  else
    raise
  end
end
attribute(name) click to toggle source
# File lib/capybara/playwright/node.rb, line 148
        def attribute(name)
  @element.get_attribute(name)
end
capybara_default_wait_time() click to toggle source
# File lib/capybara/playwright/node.rb, line 97
        def capybara_default_wait_time
  Capybara.default_max_wait_time * 1100 # with 10% buffer for allowing overhead.
end
parent_select_element() click to toggle source
# File lib/capybara/playwright/node.rb, line 339
        def parent_select_element
  @element.query_selector('xpath=ancestor::select')
end
property(name) click to toggle source
# File lib/capybara/playwright/node.rb, line 143
        def property(name)
  value = @element.get_property(name)
  value.evaluate("value => ['object', 'function'].includes(typeof value) ? null : value")
end
scroll_element_to_location(element, location) click to toggle source
# File lib/capybara/playwright/node.rb, line 719
        def scroll_element_to_location(element, location)
  scroll_opts =
    case location
    when :top
      'true'
    when :bottom
      'false'
    when :center
      "{behavior: 'instant', block: 'center'}"
    else
      raise ArgumentError, "Invalid scroll_to location: #{location}"
    end

  element.native.evaluate("(el) => { el.scrollIntoView(#{scroll_opts}) }")
end
scroll_to_coords(x, y) click to toggle source
# File lib/capybara/playwright/node.rb, line 755
              def scroll_to_coords(x, y)
        js = <<~JAVASCRIPT
        (el, [x, y]) => {
          if (el.scrollTo){
            el.scrollTo(x, y);
          } else {
            el.scrollTop = y;
            el.scrollLeft = x;
          }
        }
        JAVASCRIPT

        @element.evaluate(js, arg: [x, y])
      end
scroll_to_location(location) click to toggle source
# File lib/capybara/playwright/node.rb, line 741
              def scroll_to_location(location)
        position = SCROLL_POSITIONS[location]

        @element.evaluate(<<~JAVASCRIPT)
        (el) => {
          if (el.scrollTo){
            el.scrollTo(0, #{position});
          } else {
            el.scrollTop = #{position};
          }
        }
        JAVASCRIPT
      end