Class: Capybara::Dommy::Node

Inherits:
Capybara::Driver::Node
  • Object
show all
Defined in:
lib/capybara/dommy/node.rb

Overview

Wraps a Dommy::Element as a Capybara driver node. Modeled on Capybara::RackTest::Node: field setting mutates the Dommy::Element in the live document, while link clicks and form submission delegate to the dommy-rack session (which re-reads the same document at submit time).

Constant Summary collapse

OPTION_OWNER_XPATH =
"./parent::*[self::optgroup | self::select | self::datalist]"
DISABLED_BY_FIELDSET_XPATH =
"./parent::fieldset[./@disabled] | " \
"./ancestor::*[(not(./self::legend) or ./preceding-sibling::legend)][./parent::fieldset[./@disabled]]"
DISABLEABLE_ELEMENTS =

disabled only applies to form-associated elements; on anything else (e.g. a link that incorrectly carries the attribute) it has no effect.

%w[button fieldset input optgroup option select textarea].freeze
NON_READONLY_TYPES =

readonly does not apply to these input types, so they are never readonly even if the attribute is present (matches RackTest).

%w[hidden range color checkbox radio file submit image reset button].freeze

Instance Method Summary collapse

Instance Method Details

#==(other) ⇒ Object

Identity matters to Capybara (e.g. the focused: filter compares a candidate against session.active_element). Dommy's wrapper cache hands out one wrapper per DOM node, so native equality is node identity.



230
231
232
# File 'lib/capybara/dommy/node.rb', line 230

def ==(other)
  other.is_a?(Node) && native == other.native
end

#[](name) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/capybara/dommy/node.rb', line 19

def [](name)
  key = name.to_s
  # Capybara's validation_message filter reads the constraint-validation
  # DOM property, which never appears as a markup attribute.
  if key == "validationMessage" && native.respond_to?(:validation_message)
    return native.validation_message
  end

  # `checked` / `selected` reflect the live IDL property (current state),
  # not the content attribute (which is the *default*) — as WebDriver's
  # getAttribute does for these boolean attributes.
  if key == "checked" && native.respond_to?(:checked)
    return native.checked ? "true" : nil
  end
  if key == "selected" && native.respond_to?(:selected)
    return native.selected ? "true" : nil
  end

  native.get_attribute(key)
end

#all_textObject



50
51
52
# File 'lib/capybara/dommy/node.rb', line 50

def all_text
  text_extractor.all_text(native)
end

#checked?Boolean

Returns:

  • (Boolean)


62
63
64
# File 'lib/capybara/dommy/node.rb', line 62

def checked?
  native.respond_to?(:checked) && native.checked
end

#click(_keys = [], **_options) ⇒ Object

--- Interaction ---



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/capybara/dommy/node.rb', line 144

def click(_keys = [], **_options)
  return js_click if driver.javascript?

  if link?
    click_link_node
  elsif submits?
    submit_owning_form
  elsif checkable?
    set(!checked?)
  elsif tag_name == "label"
    click_label
  elsif (details = native.closest("details"))
    toggle_details(details)
  end
end

#disabled?Boolean

Returns:

  • (Boolean)


74
75
76
77
78
79
80
81
82
83
84
# File 'lib/capybara/dommy/node.rb', line 74

def disabled?
  return false unless DISABLEABLE_ELEMENTS.include?(tag_name)
  return true if native.has_attribute?("disabled")

  if %w[option optgroup].include?(tag_name)
    owner = native.xpath(OPTION_OWNER_XPATH).first
    owner ? self.class.new(driver, owner).disabled? : false
  else
    !native.xpath(DISABLED_BY_FIELDSET_XPATH).empty?
  end
end

#find_css(locator, **_options) ⇒ Object

--- Scoped queries (for within) ---



204
205
206
# File 'lib/capybara/dommy/node.rb', line 204

def find_css(locator, **_options)
  native.query_selector_all(locator).map { |element| self.class.new(driver, element) }
end

#find_xpath(locator, **_options) ⇒ Object



208
209
210
# File 'lib/capybara/dommy/node.rb', line 208

def find_xpath(locator, **_options)
  native.xpath(locator).map { |element| self.class.new(driver, element) }
end

#hoverObject

Move the (virtual) pointer over this element: :hover rules and matches(":hover") then apply to it and its ancestors. No mouseover/mouseout events are dispatched (nothing observes them without JavaScript).



197
198
199
200
# File 'lib/capybara/dommy/node.rb', line 197

def hover
  native.owner_document.__internal_set_hovered_element__(native)
  nil
end

#pathObject



96
97
98
99
100
101
102
103
# File 'lib/capybara/dommy/node.rb', line 96

def path
  # Capybara's documented placeholder: a shadow tree has no XPath.
  if native.respond_to?(:get_root_node) && native.get_root_node.is_a?(::Dommy::ShadowRoot)
    return "(: Shadow DOM element - no XPath :)"
  end

  native.path
end

#readonly?Boolean

Returns:

  • (Boolean)


90
91
92
93
94
# File 'lib/capybara/dommy/node.rb', line 90

def readonly?
  return false if input_field? && NON_READONLY_TYPES.include?(field_type)

  native.has_attribute?("readonly")
end

#select_optionObject



176
177
178
179
180
181
182
183
# File 'lib/capybara/dommy/node.rb', line 176

def select_option
  return if disabled?

  select_el = select_node
  deselect_all(select_el) unless select_el&.multiple
  native.selected = true
  notify_select_changed(select_el)
end

#selected?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/capybara/dommy/node.rb', line 66

def selected?
  native.respond_to?(:selected) && native.selected
end

#send_keys(*args) ⇒ Object

Keyboard input without a JS engine: maintains a caret over the field's value and applies printable keys plus the position/modifier keys Capybara's non-JS send_keys specs use. Key events are not dispatched (nothing here can observe them without JavaScript).



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/capybara/dommy/node.rb', line 109

def send_keys(*args)
  # Under JavaScript, dispatch real keyboard events (keydown/keypress/
  # input/keyup with browser default actions) through the dommy driver,
  # so keyboard handlers (arrow navigation, Enter selection) run.
  # Key chords ([:shift, "o"]) are not supported on this path.
  return driver.rack_session.send_keys_to(native, *args) if driver.javascript?

  return unless native.respond_to?(:value=)

  state = {chars: native.value.to_s.chars, caret: native.value.to_s.length, shift: false}
  args.each do |arg|
    if arg.is_a?(Array)
      # A chord like [:shift, 'o'] holds its modifiers only for the
      # duration of the array.
      held = state[:shift]
      arg.each { |key| apply_key(state, key) }
      state[:shift] = held
    else
      apply_key(state, arg)
    end
  end
  native.focus if native.respond_to?(:focus)
  native.value = state[:chars].join
end

#set(value, **_options) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/capybara/dommy/node.rb', line 160

def set(value, **_options)
  return if disabled? || readonly?

  if radio?
    set_radio
  elsif checkbox?
    set_checkbox(value)
  elsif range?
    set_range(value)
  elsif file?
    set_file(value)
  elsif input_field? || textarea?
    set_text_value(value)
  end
end

#style(styles) ⇒ Object

Computed styles for Capybara's matches_style? / style: filters, served by Dommy's CSS cascade (values come back in the computed serialization, e.g. colors as rgb()).



137
138
139
140
# File 'lib/capybara/dommy/node.rb', line 137

def style(styles)
  computed = ::Dommy::Internal::CSS::Cascade.computed_style(native)
  Array(styles).flatten.to_h { |name| [name.to_s, computed[name.to_s].to_s] }
end

#tag_nameObject



15
16
17
# File 'lib/capybara/dommy/node.rb', line 15

def tag_name
  native.tag_name.downcase
end

#unselect_optionObject



185
186
187
188
189
190
191
# File 'lib/capybara/dommy/node.rb', line 185

def unselect_option
  unless select_node&.multiple
    raise Capybara::UnselectNotAllowed, "Cannot unselect option from a non-multiple select box"
  end

  native.selected = false
end

#valueObject



40
41
42
43
44
45
46
47
48
# File 'lib/capybara/dommy/node.rb', line 40

def value
  if select?
    native.multiple ? native.selected_options.map(&:value) : native.value
  elsif checkable?
    native.has_attribute?("value") ? native.get_attribute("value") : "on"
  elsif native.respond_to?(:value)
    native.value
  end
end

#visible?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/capybara/dommy/node.rb', line 58

def visible?
  driver.visible?(native)
end

#visible_textObject



54
55
56
# File 'lib/capybara/dommy/node.rb', line 54

def visible_text
  text_extractor.visible_text(native)
end