123456789_123456789_123456789_123456789_123456789_

Class: Capybara::Selenium::Node

Relationships & Source Files
Namespace Children
Modules:
Classes:
Extension / Inclusion / Inheritance Descendants
Subclasses:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Driver::Node
Instance Chain:
Inherits: Capybara::Driver::Node
Defined in: lib/capybara/selenium/node.rb,
lib/capybara/selenium/extensions/file_input_click_emulation.rb,
lib/capybara/selenium/extensions/html5_drag.rb,
lib/capybara/selenium/extensions/modifier_keys_stack.rb

Constant Summary

  • GET_XPATH_SCRIPT =
    # File 'lib/capybara/selenium/node.rb', line 513
    <<~'JS'
      (function(el, xml){
        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;
      })(arguments[0], document)
    JS
  • OBSCURED_OR_OFFSET_SCRIPT =
    # File 'lib/capybara/selenium/node.rb', line 545
    <<~JS
      (function(el, x, y) {
        var box = el.getBoundingClientRect();
        if (x == null) x = box.width/2;
        if (y == null) y = box.height/2 ;
    
        var px = box.left + x,
            py = box.top + y,
            e = document.elementFromPoint(px, py);
    
        if (!el.contains(e))
          return true;
    
        return { x: px, y: py };
      })(arguments[0], arguments[1], arguments[2])
    JS
  • RAPID_APPEND_TEXT =
    # File 'lib/capybara/selenium/node.rb', line 562
    <<~JS
      (function(el, value) {
        value = el.value + value;
        if (el.maxLength && el.maxLength != -1){
          value = value.slice(0, el.maxLength);
        }
        el.value = value;
      })(arguments[0], arguments[1])
    JS

Node::WhitespaceNormalizer - Included

BREAKING_SPACES, EMPTY_LINES, LEADING_SPACES, LEFT_TO_RIGHT_MARK, LINE_SEPERATOR, NON_BREAKING_SPACE, PARAGRAPH_SEPERATOR, REMOVED_CHARACTERS, RIGHT_TO_LEFT_MARK, SQUEEZED_SPACES, TRAILING_SPACES, ZERO_WIDTH_SPACE

Scroll - Included

SCROLL_POSITIONS

Class Method Summary

Driver::Node - Inherited

Instance Attribute Summary

Instance Method Summary

Scroll - Included

Find - Included

Node::WhitespaceNormalizer - Included

#normalize_spacing

Normalizes the spacing of a node’s text to be similar to what matchers might expect.

#normalize_visible_spacing

Variant on Capybara::Node::Normalizer#normalize_spacing that targets the whitespace of visible elements only.

Driver::Node - Inherited

Constructor Details

This class inherits a constructor from Capybara::Driver::Node

Instance Attribute Details

#checked? (readonly)

Alias for #selected?.

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 191

alias :checked? :selected?

#content_editable?Boolean (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 200

def content_editable?
  native.attribute('isContentEditable') == 'true'
end

#disabled?Boolean (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 193

def disabled?
  return true unless native.enabled?

  # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
  find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
end

#multiple?Boolean (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 189

def multiple?; boolean_attr(self[:multiple]); end

#readonly?Boolean (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 188

def readonly?; boolean_attr(self[:readonly]); end

#selected?Boolean (readonly) Also known as: #checked?

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 190

def selected?; boolean_attr(native.selected?); end

#shadow_root (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 219

def shadow_root
  root = native.shadow_root
  root && build_node(native.shadow_root)
end

#shadow_root?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 509

def shadow_root?
  defined?(::Selenium::WebDriver::ShadowRoot) && native.is_a?(::Selenium::WebDriver::ShadowRoot)
end

#visible?Boolean (readonly)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 187

def visible?; boolean_attr(native.displayed?); end

Instance Method Details

#[](name)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 25

def [](name)
  native.attribute(name.to_s)
rescue Selenium::WebDriver::Error::WebDriverError
  nil
end

#action_pause(action, duration) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 464

def action_pause(action, duration)
  action.pause(device: action.pointer_inputs.first, duration: duration)
end

#action_with_modifiers(click_options) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 411

def action_with_modifiers(click_options)
  actions = browser_action.tap do |acts|
    if click_options.coords?
      if click_options.center_offset?
        acts.move_to(native, *click_options.coords)
      else
        right_by, down_by = *click_options.coords
        size = native.size
        left_offset = (size[:width] / 2).to_i
        top_offset = (size[:height] / 2).to_i
        left = -left_offset + right_by
        top = -top_offset + down_by
        acts.move_to(native, left, top)
      end
    else
      acts.move_to(native)
    end
  end
  modifiers_down(actions, click_options.keys)
  yield actions
  modifiers_up(actions, click_options.keys)
  actions.perform
ensure
  act = browser_action
  act.release_actions if act.respond_to?(:release_actions)
end

#all_text

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 20

def all_text
  text = driver.evaluate_script('arguments[0].textContent', self) || ''
  normalize_spacing(text)
end

#attrs(*attr_names) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 491

def attrs(*attr_names)
  return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']

  driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
    (function(el, names){
      return names.map(function(name){
        return el[name]
      });
    })(arguments[0], arguments[1]);
  JS
end

#auto_rapid_set_length (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 295

def auto_rapid_set_length
  30
end

#boolean_attr(val) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 264

def boolean_attr(val)
  val && (val != 'false')
end

#bridge (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 452

def bridge
  browser.send(:bridge)
end

#browser (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 448

def browser
  driver.browser
end

#browser_action (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 456

def browser_action
  browser.action
end

#build_node(native_node, initial_cache = {}) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 487

def build_node(native_node, initial_cache = {})
  self.class.new(driver, native_node, initial_cache)
end

#capabilities (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 460

def capabilities
  browser.capabilities
end

#click(keys = [], **options)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 106

def click(keys = [], **options)
  click_options = ClickOptions.new(keys, options)
  return native.click if click_options.empty?

  perform_with_options(click_options) do |action|
    target = click_options.coords? ? nil : native
    if click_options.delay.zero?
      action.click(target)
    else
      action.click_and_hold(target)
      action_pause(action, click_options.delay)
      action.release
    end
  end
rescue StandardError => e
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
     e.message.include?('Other element would receive the click')
    scroll_to_center
  end

  raise e
end

#double_click(keys = [], **options)

Raises:

  • (ArgumentError)
[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 144

def double_click(keys = [], **options)
  click_options = ClickOptions.new(keys, options)
  raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?

  perform_with_options(click_options) do |action|
    click_options.coords? ? action.double_click : action.double_click(native)
  end
end

#drag_to(element, drop_modifiers: [])

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 161

def drag_to(element, drop_modifiers: [], **)
  drop_modifiers = Array(drop_modifiers)
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
  # which means Seleniums `drag_and_drop` is now broken - do it manually
  scroll_if_needed { browser_action.click_and_hold(native).perform }
  # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
  element.scroll_if_needed do
    keys_down = modifiers_down(browser_action, drop_modifiers)
    keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
    keys_up.perform
  end
end

#drop(*_)

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 174

def drop(*_)
  raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
end

#each_key(keys, &block) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 479

def each_key(keys, &block)
  normalize_keys(keys).each(&block)
end

#find_context (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 483

def find_context
  native
end

#hover

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 157

def hover
  scroll_if_needed { browser_action.move_to(native).perform }
end

#modifiers_down(actions, keys) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 438

def modifiers_down(actions, keys)
  each_key(keys) { |key| actions.key_down(key) }
  actions
end

#modifiers_up(actions, keys) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 443

def modifiers_up(actions, keys)
  each_key(keys) { |key| actions.key_up(key) }
  actions
end

#native_id (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 503

def native_id
  # Selenium 3 -> 4 changed the return of ref
  type_or_id, id = native.ref
  id || type_or_id
end

#normalize_keys(keys) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 468

def normalize_keys(keys)
  keys.map do |key|
    case key
    when :ctrl then :control
    when :command, :cmd then :meta
    else
      key
    end
  end
end

#obscured?(x: nil, y: nil) ⇒ Boolean

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 208

def obscured?(x: nil, y: nil)
  res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
  return true if res == true

  driver.frame_obscured_at?(x: res['x'], y: res['y'])
end

#path

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 204

def path
  driver.evaluate_script GET_XPATH_SCRIPT, self
end

#perform_with_options(click_options, &block) (private)

Raises:

  • (ArgumentError)
[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 299

def perform_with_options(click_options, &block)
  raise ArgumentError, 'A block must be provided' unless block

  scroll_if_needed do
    action_with_modifiers(click_options) do |action|
      if block
        yield action
      else
        click_options.coords? ? action.click : action.click(native)
      end
    end
  end
end

#rect

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 215

def rect
  native.rect
end

#right_click(keys = [], **options)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 129

def right_click(keys = [], **options)
  click_options = ClickOptions.new(keys, options)
  perform_with_options(click_options) do |action|
    target = click_options.coords? ? nil : native
    if click_options.delay.zero?
      action.context_click(target)
    else
      action.move_to(target) if target
      action.pointer_down(:right).then do |act|
        action_pause(act, click_options.delay)
      end.pointer_up(:right)
    end
  end
end

#select_node (private)

a reference to the select node if this is an option node

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 269

def select_node
  find_xpath(XPath.ancestor(:select)[1]).first
end

#select_option

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 96

def select_option
  click unless selected? || disabled?
end

#send_keys(*args)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 153

def send_keys(*args)
  native.send_keys(*args)
end

#set(value, **options)

Set the value of the form element to the given value.

Parameters:

  • value (String)

    The new value

  • options (Hash{})

    Driver specific options for how to set the value

Options Hash (**options):

  • :clear (Symbol, Array) — default: nil

    The method used to clear the previous value <br/> nil => clear via javascript <br/> :none => append the new value to the existing value <br/> :backspace => send backspace keystrokes to clear the field <br/> Array => an array of keys to send before the value being set, e.g. [[:command, ‘a’], :backspace]

  • :rapid (Boolean) — default: nil

    Whether setting text inputs should use a faster &quot;rapid&quot; mode<br/> nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode<br/> true => Rapid mode will be used regardless of input length<br/> false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain

    Javascript interactions on form inputs<br/>
[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 59

def set(value, **options)
  if value.is_a?(Array) && !multiple?
    raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
  end

  tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
  @tag_name ||= tag_name

  case tag_name
  when 'input'
    case type
    when 'radio'
      click
    when 'checkbox'
      click if value ^ checked?
    when 'file'
      set_file(value)
    when 'date'
      set_date(value)
    when 'time'
      set_time(value)
    when 'datetime-local'
      set_datetime_local(value)
    when 'color'
      set_color(value)
    when 'range'
      set_range(value)
    else
      set_text(value, **options)
    end
  when 'textarea'
    set_text(value, **options)
  else
    set_content_editable(value)
  end
end

#set_color(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 337

def set_color(value) # rubocop:disable Naming/AccessorMethodName
  update_value_js(value)
end

#set_content_editable(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 387

def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
  # Ensure we are focused on the element
  click

  editable = driver.execute_script <<-JS, self
    if (arguments[0].isContentEditable) {
      var range = document.createRange();
      var sel = window.getSelection();
      arguments[0].focus();
      range.selectNodeContents(arguments[0]);
      sel.removeAllRanges();
      sel.addRange(range);
      return true;
    }
    return false;
  JS

  # The action api has a speed problem but both chrome and firefox 58 raise errors
  # if we use the faster direct send_keys.  For now just send_keys to the element
  # we've already focused.
  # native.send_keys(value.to_s)
  browser_action.send_keys(value.to_s).perform if editable
end

#set_date(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 313

def set_date(value) # rubocop:disable Naming/AccessorMethodName
  value = SettableValue.new(value)
  return set_text(value) unless value.dateable?

  # TODO: this would be better if locale can be detected and correct keystrokes sent
  update_value_js(value.to_date_str)
end

#set_datetime_local(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 329

def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
  value = SettableValue.new(value)
  return set_text(value) unless value.timeable?

  # TODO: this would be better if locale can be detected and correct keystrokes sent
  update_value_js(value.to_datetime_str)
end

#set_file(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 359

def set_file(value) # rubocop:disable Naming/AccessorMethodName
  with_file_detector do
    path_names = value.to_s.empty? ? [] : value
    file_names = Array(path_names).map do |pn|
      Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
    end.join("\n")
    native.send_keys(file_names)
  end
end

#set_range(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 341

def set_range(value) # rubocop:disable Naming/AccessorMethodName
  update_value_js(value)
end

#set_text(value, clear: nil, rapid: nil, **_unused) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 273

def set_text(value, clear: nil, rapid: nil, **_unused)
  value = value.to_s
  if value.empty? && clear.nil?
    native.clear
  elsif clear == :backspace
    # Clear field by sending the correct number of backspace keys.
    backspaces = [:backspace] * self.value.to_s.length
    send_keys(:end, *backspaces, value)
  elsif clear.is_a? Array
    send_keys(*clear, value)
  else
    driver.execute_script 'arguments[0].select()', self unless clear == :none
    if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
      send_keys(value[0..3])
      driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
      send_keys(value[-3..])
    else
      send_keys(value)
    end
  end
end

#set_time(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 321

def set_time(value) # rubocop:disable Naming/AccessorMethodName
  value = SettableValue.new(value)
  return set_text(value) unless value.timeable?

  # TODO: this would be better if locale can be detected and correct keystrokes sent
  update_value_js(value.to_time_str)
end

#sibling_index(parent, node, selector) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 250

def sibling_index(parent, node, selector)
  siblings = parent.find_xpath(selector)
  case siblings.size
  when 0
    '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
  when 1
    '' # index not necessary when only one matching element
  else
    idx = siblings.index(node)
    # Element may not be found in the siblings if it has gone away
    idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
  end
end

#style(styles)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 39

def style(styles)
  styles.to_h { |style| [style, native.css_value(style)] }
end

#tag_name

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 178

def tag_name
  @tag_name ||=
    if native.respond_to? :tag_name
      native.tag_name.downcase
    else
      shadow_root? ? 'ShadowRoot' : 'Unknown'
    end
end

#unselect_option

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 100

def unselect_option
  raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?

  click if selected?
end

#update_value_js(value) (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 345

def update_value_js(value)
  driver.execute_script(<<-JS, self, value)
    if (arguments[0].readOnly) { return };
    if (document.activeElement !== arguments[0]){
      arguments[0].focus();
    }
    if (arguments[0].value != arguments[1]) {
      arguments[0].value = arguments[1]
      arguments[0].dispatchEvent(new InputEvent('input'));
      arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
    }
  JS
end

#value

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 31

def value
  if tag_name == 'select' && multiple?
    native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text }
  else
    native[:value]
  end
end

#visible_text

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 14

def visible_text
  raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?

  native.text
end

#with_file_detector (private)

[ GitHub ]

  
# File 'lib/capybara/selenium/node.rb', line 369

def with_file_detector
  if driver.options[:browser] == :remote &&
     bridge.respond_to?(:file_detector) &&
     bridge.file_detector.nil?
    begin
      bridge.file_detector = lambda do |(fn, *)|
        str = fn.to_s
        str if File.exist?(str)
      end
      yield
    ensure
      bridge.file_detector = nil
    end
  else
    yield
  end
end