class Capybara::Selenium::Node
def ==(other)
def ==(other) native == other.native end
def [](name)
def [](name) native.attribute(name.to_s) rescue Selenium::WebDriver::Error::WebDriverError nil end
def action_with_modifiers(click_options)
def action_with_modifiers(click_options) actions = browser_action.move_to(native, *click_options.coords) 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
def all_text
def all_text text = driver.execute_script('return arguments[0].textContent', self) text.gsub(/[\u200b\u200e\u200f]/, '') .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ') .gsub(/\A[[:space:]&&[^\u00a0]]+/, '') .gsub(/[[:space:]&&[^\u00a0]]+\z/, '') .tr("\u00a0", ' ') end
def boolean_attr(val)
def boolean_attr(val) val && (val != 'false') end
def browser_action
def browser_action driver.browser.action end
def click(keys = [], **options)
def click(keys = [], **options) click_options = ClickOptions.new(keys, options) return native.click if click_options.empty? click_with_options(click_options) rescue StandardError => err if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) || err.message =~ /Other element would receive the click/ scroll_to_center end raise err end
def click_with_options(click_options)
def click_with_options(click_options) scroll_if_needed do action_with_modifiers(click_options) do |action| if block_given? yield action else click_options.coords? ? action.click : action.click(native) end end end end
def content_editable?
def content_editable? native.attribute('isContentEditable') end
def disabled?
def disabled? return true unless native.enabled? # WebDriver only defines `disabled?` for form controls but fieldset makes sense too tag_name == 'fieldset' && find_xpath('ancestor-or-self::fieldset[@disabled]').any? end
def double_click(keys = [], **options)
def double_click(keys = [], **options) click_options = ClickOptions.new(keys, options) click_with_options(click_options) do |action| click_options.coords? ? action.double_click : action.double_click(native) end end
def drag_to(element)
def drag_to(element) # 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 } end
def each_key(keys)
def each_key(keys) keys.each do |key| key = case key when :ctrl then :control when :command, :cmd then :meta else key end yield key end end
def find_css(locator)
def find_css(locator) native.find_elements(:css, locator).map { |el| self.class.new(driver, el) } end
def find_xpath(locator)
def find_xpath(locator) native.find_elements(:xpath, locator).map { |el| self.class.new(driver, el) } end
def hover
def hover scroll_if_needed { browser_action.move_to(native).perform } end
def modifiers_down(actions, keys)
def modifiers_down(actions, keys) each_key(keys) { |key| actions.key_down(key) } end
def modifiers_up(actions, keys)
def modifiers_up(actions, keys) each_key(keys) { |key| actions.key_up(key) } end
def multiple?; boolean_attr(self[:multiple]); end
def multiple?; boolean_attr(self[:multiple]); end
def path
def path path = find_xpath(XPath.ancestor_or_self).reverse result = [] default_ns = path.last[:namespaceURI] while (node = path.shift) parent = path.first selector = node[:tagName] if node[:namespaceURI] != default_ns selector = XPath.child.where((XPath.local_name == selector) & (XPath.namespace_uri == node[:namespaceURI])).to_s selector end if parent siblings = parent.find_xpath(selector) 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 "[#{siblings.index(node) + 1}]" end end result.push selector end '/' + result.reverse.join('/') end
def readonly?; boolean_attr(self[:readonly]); end
def readonly?; boolean_attr(self[:readonly]); end
def right_click(keys = [], **options)
def right_click(keys = [], **options) click_options = ClickOptions.new(keys, options) click_with_options(click_options) do |action| click_options.coords? ? action.context_click : action.context_click(native) end end
def scroll_if_needed
def scroll_if_needed yield rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError scroll_to_center yield end
def scroll_to_center
def scroll_to_center script = <<-'JS' try { arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'}); } catch(e) { arguments[0].scrollIntoView(true); } JS begin driver.execute_script(script, self) rescue StandardError # rubocop:disable Lint/HandleExceptions # Swallow error if scrollIntoView with options isn't supported end end
def select_node
def select_node find_xpath(XPath.ancestor(:select)[1]).first end
def select_option
def select_option click unless selected? || disabled? end
def selected?; boolean_attr(native.selected?); end
def selected?; boolean_attr(native.selected?); end
def send_keys(*args)
def send_keys(*args) native.send_keys(*args) end
def set(value, **options)
(**options)
-
:clear
(Symbol, Array
) -- The method used to clear the previous value
Parameters:
-
options
(Hash{}
) -- Driver specific options for how to set the value -
value
(String
) -- The new value
def set(value, **options) raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple? case tag_name when 'input' case self[: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) else set_text(value, options) end when 'textarea' set_text(value, options) else set_content_editable(value) if content_editable? end end
def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName # Ensure we are focused on the element click script = <<-JS var range = document.createRange(); var sel = window.getSelection(); arguments[0].focus(); range.selectNodeContents(arguments[0]); sel.removeAllRanges(); sel.addRange(range); JS driver.execute_script script, self # 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 end
def set_date(value) # rubocop:disable Naming/AccessorMethodName
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
def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
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
def set_file(value) # rubocop:disable Naming/AccessorMethodName
def set_file(value) # rubocop:disable Naming/AccessorMethodName path_names = value.to_s.empty? ? [] : value native.send_keys(Array(path_names).join("\n")) end
def set_text(value, clear: nil, **_unused)
def set_text(value, clear: 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 # 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 "arguments[0].value = ''", self unless clear == :none send_keys(value) end end
def set_time(value) # rubocop:disable Naming/AccessorMethodName
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
def style(styles)
def style(styles) styles.each_with_object({}) do |style, result| result[style] = native.css_value(style) end end
def tag_name
def tag_name native.tag_name.downcase end
def unselect_option
def unselect_option raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple? click if selected? end
def update_value_js(value)
def update_value_js(value) driver.execute_script(<<-JS, self, value) 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
def value
def value if tag_name == 'select' && multiple? native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text } else native[:value] end end
def visible?; boolean_attr(native.displayed?); end
def visible?; boolean_attr(native.displayed?); end
def visible_text
def visible_text native.text end