class Capybara::Apparition::Node

Constants

CLEAR_ELEMENT_JS
CURRENT_NODE_SELECTED_JS
DELETE_TEXT_JS
DISPATCH_EVENT_JS
ELEMENT_DISABLED_JS
ELEMENT_PROP_OR_ATTR_JS
ELEMENT_VISIBLE_TEXT_JS
EVENTS
FIND_CSS_JS
FIND_XPATH_JS
GET_ATTRIBUTES_JS
GET_CLIENT_RECT_JS
GET_PATH_JS

JS snippets

GET_STYLES_JS
GET_VALUE_JS
SELECT_OPTION_JS
UNSELECT_OPTION_JS
VISIBLE_JS

if an area element, check visibility of relevant image

Attributes

page_id[R]

Public Class Methods

new(driver, page, remote_object, initial_cache) click to toggle source
Calls superclass method
# File lib/capybara/apparition/node.rb, line 14
def initialize(driver, page, remote_object, initial_cache)
  super(driver, self, initial_cache)
  @page = page
  @remote_object = remote_object
end

Public Instance Methods

==(other) click to toggle source
# File lib/capybara/apparition/node.rb, line 272
def ==(other)
  evaluate_on('el => this == el', objectId: other.id)
rescue ObsoleteNode
  false
end
[](name) click to toggle source
# File lib/capybara/apparition/node.rb, line 99
def [](name)
  # Although the attribute matters, the property is consistent. Return that in
  # preference to the attribute for links and images.
  evaluate_on ELEMENT_PROP_OR_ATTR_JS, value: name
end
all_text() click to toggle source
# File lib/capybara/apparition/node.rb, line 50
def all_text
  text = evaluate_on('() => this.textContent')
  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
attribute(name) click to toggle source
# File lib/capybara/apparition/node.rb, line 91
def attribute(name)
  if %w[checked selected].include?(name.to_s)
    property(name)
  else
    evaluate_on('name => this.getAttribute(name)', value: name)
  end
end
attributes() click to toggle source
# File lib/capybara/apparition/node.rb, line 105
def attributes
  evaluate_on GET_ATTRIBUTES_JS
end
checked?() click to toggle source
# File lib/capybara/apparition/node.rb, line 188
def checked?
  self[:checked]
end
click(keys = [], button: 'left', count: 1, delay: 0, **options) click to toggle source
# File lib/capybara/apparition/node.rb, line 200
def click(keys = [], button: 'left', count: 1, delay: 0, **options)
  pos = element_click_pos(**options)
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if pos.nil?

  test = mouse_event_test(**pos)
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if test.nil?

  unless options[:x] && options[:y]
    raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
  end

  @page.mouse.click_at(**pos.merge(button: button, count: count, modifiers: keys, delay: delay))
  if ENV['DEBUG']
    begin
      new_pos = element_click_pos(**options)
      puts "Element moved from #{pos} to #{new_pos}" unless pos == new_pos
    rescue WrongWorld # rubocop:disable Lint/SuppressedException
    end
  end
  # Wait a short time to see if click triggers page load
  sleep 0.05
  @page.wait_for_loaded(allow_obsolete: true)
end
disabled?() click to toggle source
# File lib/capybara/apparition/node.rb, line 196
def disabled?
  evaluate_on ELEMENT_DISABLED_JS
end
double_click(keys = [], **options) click to toggle source
# File lib/capybara/apparition/node.rb, line 228
def double_click(keys = [], **options)
  click(keys, count: 2, **options)
end
element_click_pos(x: nil, y: nil, offset: nil, **) click to toggle source
# File lib/capybara/apparition/node.rb, line 288
def element_click_pos(x: nil, y: nil, offset: nil, **)
  if x && y
    if offset == :center
      visible_center
    else
      visible_top_left
    end.tap do |p|
      p[:x] += x
      p[:y] += y
    end
  else
    visible_center
  end
end
find(method, selector) click to toggle source
# File lib/capybara/apparition/node.rb, line 30
def find(method, selector)
  js = method == :css ? FIND_CSS_JS : FIND_XPATH_JS
  evaluate_on(js, value: selector).map do |r_o|
    tag_name = r_o['description'].split(/[.#]/, 2)[0]
    Capybara::Apparition::Node.new(driver, @page, r_o['objectId'], tag_name: tag_name)
  end
rescue ::Capybara::Apparition::BrowserError => e
  raise unless /is not a valid (XPath expression|selector)/.match? e.name

  raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
end
find_css(selector) click to toggle source
# File lib/capybara/apparition/node.rb, line 46
def find_css(selector)
  find :css, selector
end
find_xpath(selector) click to toggle source
# File lib/capybara/apparition/node.rb, line 42
def find_xpath(selector)
  find :xpath, selector
end
hover() click to toggle source
# File lib/capybara/apparition/node.rb, line 232
def hover
  pos = visible_center
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?

  @page.mouse.move_to(**pos)
end
id() click to toggle source
# File lib/capybara/apparition/node.rb, line 22
def id
  @remote_object
end
inner_html() click to toggle source

capybara-webkit method

# File lib/capybara/apparition/node.rb, line 76
def inner_html
  self[:innerHTML]
end
inner_html=(value) click to toggle source

capybara-webkit method

# File lib/capybara/apparition/node.rb, line 81
    def inner_html=(value)
      driver.execute_script <<~JS, self, value
        arguments[0].innerHTML = arguments[1]
      JS
    end
obscured?(**) click to toggle source
# File lib/capybara/apparition/node.rb, line 173
def obscured?(**)
  pos = visible_center(allow_scroll: false)
  return true if pos.nil?

  hit_node = @page.element_from_point(**pos)
  return true if hit_node.nil?

  begin
    return evaluate_on('el => !this.contains(el)', objectId: hit_node['objectId'])
  rescue WrongWorld # rubocop:disable Lint/SuppressedException
  end

  true
end
parents() click to toggle source
# File lib/capybara/apparition/node.rb, line 26
def parents
  find('xpath', 'ancestor::*').reverse
end
path() click to toggle source
# File lib/capybara/apparition/node.rb, line 284
def path
  evaluate_on GET_PATH_JS
end
property(name) click to toggle source
# File lib/capybara/apparition/node.rb, line 87
def property(name)
  evaluate_on('name => this[name]', value: name)
end
rect() click to toggle source
# File lib/capybara/apparition/node.rb, line 387
def rect
  evaluate_on GET_CLIENT_RECT_JS
end
right_click(keys = [], **options) click to toggle source
# File lib/capybara/apparition/node.rb, line 224
def right_click(keys = [], **options)
  click(keys, button: 'right', **options)
end
scroll_by(x, y) click to toggle source
# File lib/capybara/apparition/node.rb, line 391
    def scroll_by(x, y)
      evaluate_on <<~JS, { value: x }, value: y
        (x, y) => this.scrollBy(x,y)
      JS
      self
    end
scroll_to(element, location, position = nil) click to toggle source
# File lib/capybara/apparition/node.rb, line 398
def scroll_to(element, location, position = nil)
  if element.is_a? Capybara::Apparition::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/apparition/node.rb, line 151
def select_option
  return false if disabled?

  evaluate_on SELECT_OPTION_JS
  true
end
selected?() click to toggle source
# File lib/capybara/apparition/node.rb, line 192
def selected?
  !!self[:selected]
end
send_key(*keys, delay: 0, **opts)
Alias for: send_keys
send_keys(*keys, delay: 0, **opts) click to toggle source
# File lib/capybara/apparition/node.rb, line 278
def send_keys(*keys, delay: 0, **opts)
  click unless evaluate_on CURRENT_NODE_SELECTED_JS
  _send_keys(*keys, delay: delay, **opts)
end
Also aliased as: send_key
set(value, **options) click to toggle source
# File lib/capybara/apparition/node.rb, line 117
def set(value, **options)
  if tag_name == 'input'
    case self[:type]
    when 'radio'
      click
    when 'checkbox'
      click if value != checked?
    when 'file'
      files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
      set_files(files)
    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.to_s, **{ delay: 0 }.merge(options))
    end
  elsif tag_name == 'textarea'
    set_text(value.to_s)
  elsif tag_name == 'select'
    warn "Setting the value of a select element via 'set' is deprecated, please use 'select' or 'select_option'."
    evaluate_on '()=>{ this.value = arguments[0] }', value: value.to_s
  elsif self[:isContentEditable]
    delete_text
    send_keys(value.to_s, delay: options.fetch(:delay, 0))
  end
end
style(styles) click to toggle source
# File lib/capybara/apparition/node.rb, line 113
def style(styles)
  evaluate_on GET_STYLES_JS, value: styles
end
submit() click to toggle source
# File lib/capybara/apparition/node.rb, line 268
def submit
  evaluate_on '()=>{ this.submit() }'
end
tag_name() click to toggle source
# File lib/capybara/apparition/node.rb, line 165
def tag_name
  @tag_name ||= evaluate_on('() => this.tagName').downcase
end
text() click to toggle source

capybara-webkit method

# File lib/capybara/apparition/node.rb, line 70
def text
  warn 'Node#text is deprecated, please use Node#visible_text instead'
  visible_text
end
top_left() click to toggle source
# File lib/capybara/apparition/node.rb, line 380
def top_left
  result = evaluate_on GET_CLIENT_RECT_JS
  return nil if result.nil?

  { x: result['x'], y: result['y'] }
end
trigger(name, event_type = nil, **options) click to toggle source
# File lib/capybara/apparition/node.rb, line 260
def trigger(name, event_type = nil, **options)
  raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym) || event_type

  event_type, opts = *EVENTS[name.to_sym], {} if event_type.nil?

  evaluate_on DISPATCH_EVENT_JS, { value: event_type }, { value: name }, value: opts.merge(options)
end
unselect_option() click to toggle source
# File lib/capybara/apparition/node.rb, line 158
def unselect_option
  return false if disabled?

  evaluate_on(UNSELECT_OPTION_JS) ||
    raise(Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.')
end
value() click to toggle source
# File lib/capybara/apparition/node.rb, line 109
def value
  evaluate_on GET_VALUE_JS
end
visible?() click to toggle source
# File lib/capybara/apparition/node.rb, line 169
def visible?
  evaluate_on VISIBLE_JS
end
visible_center(allow_scroll: true) click to toggle source
# File lib/capybara/apparition/node.rb, line 337
def visible_center(allow_scroll: true)
  rect = in_view_client_rect(allow_scroll: allow_scroll)
  return nil if rect.nil?

  frame_offset = @page.current_frame_offset

  if rect['width'].zero? || rect['height'].zero?
    return nil unless tag_name == 'area'

    map = find('xpath', 'ancestor::map').first
    img = find('xpath', "//img[@usemap='##{map[:name]}']").first
    return nil unless img.visible?

    img_pos = img.top_left
    coords = self[:coords].split(',').map(&:to_i)

    offset_pos = case self[:shape]
    when 'rect'
      { x: (coords[0] + coords[2]) / 2,
        y: (coords[1] + coords[2]) / 2 }
    when 'circle'
      { x: coords[0], y: coords[1] }
    when 'poly'
      raise 'TODO: Poly not implemented'
    else
      raise 'Unknown Shape'
    end

    { x: img_pos[:x] + offset_pos[:x] + frame_offset[:x],
      y: img_pos[:y] + offset_pos[:y] + frame_offset[:y] }
  else
    lm = @page.command('Page.getLayoutMetrics')
    x_extents = [rect['left'], rect['right']].minmax
    y_extents = [rect['top'], rect['bottom']].minmax

    x_extents[1] = [x_extents[1], lm['layoutViewport']['clientWidth']].min
    y_extents[1] = [y_extents[1], lm['layoutViewport']['clientHeight']].min

    { x: (x_extents.sum / 2) + frame_offset[:x],
      y: (y_extents.sum / 2) + frame_offset[:y] }
  end
end
visible_text() click to toggle source
# File lib/capybara/apparition/node.rb, line 59
def visible_text
  return '' unless visible?

  text = evaluate_on ELEMENT_VISIBLE_TEXT_JS
  text.to_s.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
      .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
      .gsub(/\n+/, "\n")
      .tr("\u00a0", ' ')
end
visible_top_left() click to toggle source
# File lib/capybara/apparition/node.rb, line 303
def visible_top_left
  rect = in_view_client_rect
  return nil if rect.nil?

  frame_offset = @page.current_frame_offset

  if rect['width'].zero? || rect['height'].zero?
    return nil unless tag_name == 'area'

    map = find('xpath', 'ancestor::map').first
    img = find('xpath', "//img[@usemap='##{map[:name]}']").first
    return nil unless img.visible?

    img_pos = img.top_left
    coords = self[:coords].split(',').map(&:to_i)

    offset_pos = case self[:shape]
    when 'rect'
      { x: coords[0], y: coords[1] }
    when 'circle'
      { x: coords[0], y: coords[1] }
    when 'poly'
      raise 'TODO: Poly not implemented'
    else
      raise 'Unknown Shape'
    end

    { x: img_pos[:x] + offset_pos[:x] + frame_offset[:x],
      y: img_pos[:y] + offset_pos[:y] + frame_offset[:y] }
  else
    { x: rect['left'] + frame_offset[:x], y: rect['top'] + frame_offset[:y] }
  end
end

Protected Instance Methods

evaluate_on(page_function, *args) click to toggle source
# File lib/capybara/apparition/node.rb, line 411
    def evaluate_on(page_function, *args)
      obsolete_checked_function = <<~JS
        function(){
          if (!this.ownerDocument.contains(this)) { throw 'ObsoleteNode' };
          return (#{page_function.strip}).apply(this, arguments);
        }
      JS
      response = @page.command('Runtime.callFunctionOn',
                               functionDeclaration: obsolete_checked_function,
                               objectId: id,
                               returnByValue: false,
                               awaitPromise: true,
                               arguments: args)
      process_response(response)
    end
scroll_if_needed() click to toggle source
# File lib/capybara/apparition/node.rb, line 427
    def scroll_if_needed
      driver.execute_script <<~JS, self
        arguments[0].scrollIntoViewIfNeeded({behavior: 'instant', block: 'center', inline: 'center'})
      JS
    end

Private Instance Methods

_send_keys(*keys, delay: 0, **_opts) click to toggle source
# File lib/capybara/apparition/node.rb, line 461
def _send_keys(*keys, delay: 0, **_opts)
  @page.keyboard.type(keys, delay: delay)
end
delete_text() click to toggle source
# File lib/capybara/apparition/node.rb, line 622
def delete_text
  evaluate_on DELETE_TEXT_JS
end
filter_text(text) click to toggle source
# File lib/capybara/apparition/node.rb, line 472
def filter_text(text)
  text.to_s.gsub(/[[:space:]]+/, ' ').strip
end
focus() click to toggle source
# File lib/capybara/apparition/node.rb, line 435
def focus
  @page.command('DOM.focus', objectId: id)
end
in_view_client_rect(allow_scroll: true) click to toggle source
# File lib/capybara/apparition/node.rb, line 465
def in_view_client_rect(allow_scroll: true)
  evaluate_on('() => this.scrollIntoViewIfNeeded()') if allow_scroll
  result = evaluate_on GET_CLIENT_RECT_JS
  result = result['model'] if result && result['model']
  result
end
keys_to_send(value, clear) click to toggle source
# File lib/capybara/apparition/node.rb, line 439
    def keys_to_send(value, clear)
      case clear
      when :backspace
        # Clear field by sending the correct number of backspace keys.
        [:end] + ([:backspace] * self.value.to_s.length) + [value]
      when :none
        [value]
      when Array
        clear << value
      else
        # Clear field by JavaScript assignment of the value property.
        # Script can change a readonly element which user input cannot, so
        # don't execute if readonly.
        driver.execute_script <<~JS, self
          if (!arguments[0].readOnly) {
            arguments[0].value = ''
          }
        JS
        [value]
      end
    end
mouse_event_test(x:, y:) click to toggle source
# File lib/capybara/apparition/node.rb, line 570
    def mouse_event_test(x:, y:)
      r_o = @page.element_from_point(x: x, y: y)
      return nil unless r_o && r_o['objectId']

      tag_name = r_o['description'].split(/[.#]/, 2)[0]
      hit_node = Capybara::Apparition::Node.new(driver, @page, r_o['objectId'], tag_name: tag_name)
      result = begin
        evaluate_on(<<~JS, objectId: hit_node.id)
          (hit_node) => {
            if ((hit_node == this) || this.contains(hit_node))
              return { status: 'success' };
            return { status: 'failure' };
          }
        JS
               rescue WrongWorld
                 { 'status': 'failure' }
      end
      OpenStruct.new(success: result['status'] == 'success', selector: r_o['description'])
    end
mouse_event_test?(x:, y:) click to toggle source
# File lib/capybara/apparition/node.rb, line 566
def mouse_event_test?(x:, y:)
  mouse_event_test(x: x, y: y).success
end
process_response(response) click to toggle source
# File lib/capybara/apparition/node.rb, line 476
def process_response(response)
  exception_details = response['exceptionDetails']
  if exception_details && (exception = exception_details['exception'])
    case exception['className']
    when 'DOMException'
      raise ::Capybara::Apparition::BrowserError.new('name' => exception['description'], 'args' => nil)
    else
      raise ::Capybara::Apparition::ObsoleteNode.new(self, '') if exception['value'] == 'ObsoleteNode'

      puts "Unknown Exception: #{exception['value']}"
    end
    raise exception_details
  end

  DevToolsProtocol::RemoteObject.new(@page, response['result'] || response['object']).value
end
scroll_element_to_location(element, location) click to toggle source
# File lib/capybara/apparition/node.rb, line 590
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.evaluate_on "() => this.scrollIntoView(#{scroll_opts})"
end
scroll_to_coords(x, y) click to toggle source
# File lib/capybara/apparition/node.rb, line 616
    def scroll_to_coords(x, y)
      evaluate_on <<~JS, { value: x }, value: y
        (x,y) => this.scrollTo(x,y)
      JS
    end
scroll_to_location(location) click to toggle source
# File lib/capybara/apparition/node.rb, line 604
def scroll_to_location(location)
  scroll_y = case location
  when :top
    '0'
  when :bottom
    'this.scrollHeight'
  when :center
    '(this.scrollHeight - this.clientHeight)/2'
  end
  evaluate_on "() => this.scrollTo(0, #{scroll_y})"
end
set_color(value) click to toggle source
# File lib/capybara/apparition/node.rb, line 543
def set_color(value)
  update_value_js(value.to_s)
end
set_date(value) click to toggle source
# File lib/capybara/apparition/node.rb, line 515
def set_date(value)
  value = SettableValue.new(value)
  unless value.dateable?
    # click(x:5, y:10)
    # debug
    return set_text(value)
  end

  # 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) click to toggle source
# File lib/capybara/apparition/node.rb, line 535
def set_datetime_local(value)
  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_files(files) click to toggle source
# File lib/capybara/apparition/node.rb, line 511
def set_files(files)
  @page.command('DOM.setFileInputFiles', files: Array(files), objectId: id)
end
set_range(value) click to toggle source
# File lib/capybara/apparition/node.rb, line 547
def set_range(value)
  update_value_js(value.to_s)
end
set_text(value, clear: nil, delay: 0, rapid: nil, **_unused) click to toggle source
# File lib/capybara/apparition/node.rb, line 493
    def set_text(value, clear: nil, delay: 0, rapid: nil, **_unused)
      value = value.to_s
      if value.empty? && clear.nil?
        evaluate_on CLEAR_ELEMENT_JS
      else
        focus
        if (rapid && (value.length >= 6)) || ((value.length > 30) && rapid != false)
          _send_keys(*keys_to_send(value[0..2], clear), delay: delay)
          driver.execute_script <<~JS, self, value[0...-3]
            arguments[0].value = arguments[1]
          JS
          _send_keys(*keys_to_send(value[-3..-1], :none), delay: delay)
        else
          _send_keys(*keys_to_send(value, clear), delay: delay)
        end
      end
    end
set_time(value) click to toggle source
# File lib/capybara/apparition/node.rb, line 527
def set_time(value)
  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
update_value_js(value) click to toggle source
# File lib/capybara/apparition/node.rb, line 551
    def update_value_js(value)
      evaluate_on(<<~JS, value: value)
        value => {
          if (document.activeElement !== this){
            this.focus();
          }
          if (this.value != value) {
            this.value = value;
            this.dispatchEvent(new InputEvent('input'));
            this.dispatchEvent(new Event('change', { bubbles: true }));
          }
        }
      JS
    end