Class: Capybara::Lightpanda::Node
- Inherits:
-
Driver::Node
- Object
- Driver::Node
- Capybara::Lightpanda::Node
- Defined in:
- lib/capybara/lightpanda/node.rb
Constant Summary collapse
- MOVING_WAIT_DELAY =
ENV.fetch("LIGHTPANDA_NODE_MOVING_WAIT", 0.01).to_f
- MOVING_WAIT_ATTEMPTS =
ENV.fetch("LIGHTPANDA_NODE_MOVING_ATTEMPTS", 50).to_i
Instance Attribute Summary collapse
-
#remote_object_id ⇒ Object
readonly
Returns the value of attribute remote_object_id.
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
Equality compares the underlying DOM node via backendNodeId, the only identity that’s stable across CDP calls.
-
#[](name) ⇒ Object
Smart property/attribute getter (Cuprite pattern).
- #all_text ⇒ Object
- #backend_node_id ⇒ Object
- #checked? ⇒ Boolean
- #click(_keys = [], **_options) ⇒ Object
- #disabled? ⇒ Boolean
- #double_click(_keys = [], **_options) ⇒ Object
- #find_css(selector) ⇒ Object
- #find_xpath(selector) ⇒ Object
-
#hash ⇒ Object
Hash on backendNodeId so equal nodes always hash the same.
- #hover ⇒ Object
-
#initialize(driver, remote_object_id) ⇒ Node
constructor
A new instance of Node.
-
#moving?(delay: MOVING_WAIT_DELAY) ⇒ Boolean
Returns true when the element’s bounding rect has changed between two samples taken ‘delay` seconds apart.
- #multiple? ⇒ Boolean
- #obscured? ⇒ Boolean
- #path ⇒ Object
- #readonly? ⇒ Boolean
- #rect ⇒ Object
- #right_click(_keys = [], **_options) ⇒ Object
- #select_option ⇒ Object
- #selected? ⇒ Boolean
- #send_keys ⇒ Object
- #set(value, **_options) ⇒ Object
- #shadow_root ⇒ Object
- #style(styles) ⇒ Object
- #tag_name ⇒ Object
- #text ⇒ Object
-
#trigger(event) ⇒ Object
Dispatch an arbitrary DOM event by name.
- #unselect_option ⇒ Object
- #value ⇒ Object
- #visible? ⇒ Boolean
-
#visible_text ⇒ Object
Lightpanda’s innerText returns textContent verbatim (no rendering, so no hidden-descendant filtering).
-
#wait_for_stop_moving(delay: MOVING_WAIT_DELAY, attempts: MOVING_WAIT_ATTEMPTS) ⇒ Object
Block until the element’s rect stabilises across two consecutive samples or ‘attempts` polls have elapsed (whichever first).
Constructor Details
#initialize(driver, remote_object_id) ⇒ Node
Returns a new instance of Node.
11 12 13 14 |
# File 'lib/capybara/lightpanda/node.rb', line 11 def initialize(driver, remote_object_id) super @remote_object_id = remote_object_id end |
Instance Attribute Details
#remote_object_id ⇒ Object (readonly)
Returns the value of attribute remote_object_id.
9 10 11 |
# File 'lib/capybara/lightpanda/node.rb', line 9 def remote_object_id @remote_object_id end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
Equality compares the underlying DOM node via backendNodeId, the only identity that’s stable across CDP calls. NO fast path on remote_object_id: two wrappers with the same remote_object_id can resolve to different backendNodeIds (one cached at 42, the other still nil from a transient describeNode failure), and a remote-id fast path there would return ‘true` while `#hash` returned different values, violating the hash contract. When either side fails to resolve, the nodes are treated as not equal so stale wrappers don’t collapse onto each other.
220 221 222 223 224 225 226 |
# File 'lib/capybara/lightpanda/node.rb', line 220 def ==(other) return false unless other.is_a?(self.class) left = backend_node_id right = other.backend_node_id !left.nil? && left == right end |
#[](name) ⇒ Object
Smart property/attribute getter (Cuprite pattern). Returns resolved URLs for src/href, raw attributes otherwise.
90 91 92 |
# File 'lib/capybara/lightpanda/node.rb', line 90 def [](name) call(PROPERTY_OR_ATTRIBUTE_JS, name.to_s) end |
#all_text ⇒ Object
21 22 23 24 |
# File 'lib/capybara/lightpanda/node.rb', line 21 def all_text ensure_connected filter_text(call("function() { return this.textContent }")) end |
#backend_node_id ⇒ Object
238 239 240 |
# File 'lib/capybara/lightpanda/node.rb', line 238 def backend_node_id @backend_node_id ||= driver.browser.backend_node_id(@remote_object_id) end |
#checked? ⇒ Boolean
178 179 180 |
# File 'lib/capybara/lightpanda/node.rb', line 178 def checked? call("function() { return this.checked }") end |
#click(_keys = [], **_options) ⇒ Object
102 103 104 105 |
# File 'lib/capybara/lightpanda/node.rb', line 102 def click(_keys = [], **) call(CLICK_JS) driver.browser.wait_for_idle end |
#disabled? ⇒ Boolean
186 187 188 |
# File 'lib/capybara/lightpanda/node.rb', line 186 def disabled? call(DISABLED_JS) end |
#double_click(_keys = [], **_options) ⇒ Object
111 112 113 |
# File 'lib/capybara/lightpanda/node.rb', line 111 def double_click(_keys = [], **) call("function() { this.dispatchEvent(new MouseEvent('dblclick', {bubbles: true, cancelable: true})) }") end |
#find_css(selector) ⇒ Object
207 208 209 210 |
# File 'lib/capybara/lightpanda/node.rb', line 207 def find_css(selector) object_ids = driver.browser.find_within(@remote_object_id, "css", selector) object_ids.map { |oid| self.class.new(driver, oid) } end |
#find_xpath(selector) ⇒ Object
202 203 204 205 |
# File 'lib/capybara/lightpanda/node.rb', line 202 def find_xpath(selector) object_ids = driver.browser.find_within(@remote_object_id, "xpath", selector) object_ids.map { |oid| self.class.new(driver, oid) } end |
#hash ⇒ Object
Hash on backendNodeId so equal nodes always hash the same. When describeNode fails (returns nil) the bucket collapses to ‘nil.hash`; combined with `==` returning false for nil-resolved nodes, Set/Hash membership stays consistent (collisions are allowed for unequal objects).
234 235 236 |
# File 'lib/capybara/lightpanda/node.rb', line 234 def hash backend_node_id.hash end |
#hover ⇒ Object
115 116 117 |
# File 'lib/capybara/lightpanda/node.rb', line 115 def hover call("function() { this.dispatchEvent(new MouseEvent('mouseover', {bubbles: true, cancelable: true})) }") end |
#moving?(delay: MOVING_WAIT_DELAY) ⇒ Boolean
Returns true when the element’s bounding rect has changed between two samples taken ‘delay` seconds apart. Lightpanda has no real animation frame loop so most “movement” is JS-driven (style mutations); this works because getBoundingClientRect reflects those mutations.
52 53 54 55 56 |
# File 'lib/capybara/lightpanda/node.rb', line 52 def moving?(delay: MOVING_WAIT_DELAY) previous = rect sleep(delay) previous != rect end |
#multiple? ⇒ Boolean
194 195 196 |
# File 'lib/capybara/lightpanda/node.rb', line 194 def multiple? call("function() { return this.multiple }") end |
#obscured? ⇒ Boolean
44 45 46 |
# File 'lib/capybara/lightpanda/node.rb', line 44 def obscured? call(OBSCURED_JS) end |
#path ⇒ Object
198 199 200 |
# File 'lib/capybara/lightpanda/node.rb', line 198 def path call(GET_PATH_JS) end |
#readonly? ⇒ Boolean
190 191 192 |
# File 'lib/capybara/lightpanda/node.rb', line 190 def readonly? call("function() { return this.readOnly }") end |
#rect ⇒ Object
40 41 42 |
# File 'lib/capybara/lightpanda/node.rb', line 40 def rect call(GET_RECT_JS) end |
#right_click(_keys = [], **_options) ⇒ Object
107 108 109 |
# File 'lib/capybara/lightpanda/node.rb', line 107 def right_click(_keys = [], **) call("function() { this.dispatchEvent(new MouseEvent('contextmenu', {bubbles: true, cancelable: true})) }") end |
#select_option ⇒ Object
142 143 144 |
# File 'lib/capybara/lightpanda/node.rb', line 142 def select_option call(SELECT_OPTION_JS) end |
#selected? ⇒ Boolean
182 183 184 |
# File 'lib/capybara/lightpanda/node.rb', line 182 def selected? call("function() { return !!this.selected }") end |
#send_keys ⇒ Object
158 159 160 161 |
# File 'lib/capybara/lightpanda/node.rb', line 158 def send_keys(*) call("function() { this.focus() }") driver.browser.keyboard.type(*) end |
#set(value, **_options) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/capybara/lightpanda/node.rb', line 127 def set(value, **) case tag_name when "input" fill_input(value) when "textarea" call(SET_VALUE_JS, truncate_to_maxlength(value.to_s)) else # `contenteditable` cascades through descendants. Check # `isContentEditable`, then fall back to walking ancestors for # `contenteditable` since Lightpanda doesn't expose the property on # every element. EDITABLE_HOST_JS encapsulates that check. call("function(v) { this.innerHTML = v }", value.to_s) if call(EDITABLE_HOST_JS) end end |
#shadow_root ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/capybara/lightpanda/node.rb', line 75 def shadow_root result = driver.browser.with_default_context_wait do driver.browser.call_function_on( @remote_object_id, "function() { return this.shadowRoot }", return_by_value: false ) end return nil unless result.is_a?(Hash) && result["objectId"] self.class.new(driver, result["objectId"]) end |
#style(styles) ⇒ Object
98 99 100 |
# File 'lib/capybara/lightpanda/node.rb', line 98 def style(styles) styles.to_h { |style| [style, call(GET_STYLE_JS, style)] } end |
#tag_name ⇒ Object
163 164 165 166 167 168 169 170 171 172 |
# File 'lib/capybara/lightpanda/node.rb', line 163 def tag_name # ShadowRoot/DocumentFragment have no tagName; report a stable label so # Capybara's failure messages can render `tag="ShadowRoot"`. # Memoized: an objectId points to a single DOM node whose tagName is # immutable for that node's lifetime. @tag_name ||= call("function() { if (this.nodeType === 11) return 'ShadowRoot'; return this.tagName ? this.tagName.toLowerCase() : ''; }") end |
#text ⇒ Object
16 17 18 19 |
# File 'lib/capybara/lightpanda/node.rb', line 16 def text ensure_connected call("function() { return this.textContent }") end |
#trigger(event) ⇒ Object
Dispatch an arbitrary DOM event by name. Mirrors Cuprite’s Node#trigger — picks the right Event constructor for known mouse/focus/form names and falls back to a generic Event for everything else (so callers can fire custom events like ‘node.trigger(’lp:custom’)‘).
123 124 125 |
# File 'lib/capybara/lightpanda/node.rb', line 123 def trigger(event) call(TRIGGER_JS, event.to_s) end |
#unselect_option ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/capybara/lightpanda/node.rb', line 146 def unselect_option unless call("function() { var s = this.parentElement; while (s && (s.tagName || '').toUpperCase() !== 'SELECT') s = s.parentElement; return !!(s && s.multiple); }") raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." end call(UNSELECT_OPTION_JS) end |
#value ⇒ Object
94 95 96 |
# File 'lib/capybara/lightpanda/node.rb', line 94 def value call(GET_VALUE_JS) end |
#visible? ⇒ Boolean
174 175 176 |
# File 'lib/capybara/lightpanda/node.rb', line 174 def visible? call(VISIBLE_JS) end |
#visible_text ⇒ Object
Lightpanda’s innerText returns textContent verbatim (no rendering, so no hidden-descendant filtering). Walk descendants ourselves, skipping nodes that fail VISIBLE_JS, and emit newlines around block-display elements (the part of innerText behavior we still need).
30 31 32 33 34 35 36 37 38 |
# File 'lib/capybara/lightpanda/node.rb', line 30 def visible_text ensure_connected call(VISIBLE_TEXT_JS).to_s .gsub(/\A[[:space:]&&[^\u00A0]]+/, "") .gsub(/[[:space:]&&[^\u00A0]]+\z/, "") .gsub(/[ \t\f\v]+/, " ") .gsub(/[ \t\f\v]*\n[ \t\f\v\n]*/, "\n") .tr("\u00A0", " ") end |
#wait_for_stop_moving(delay: MOVING_WAIT_DELAY, attempts: MOVING_WAIT_ATTEMPTS) ⇒ Object
Block until the element’s rect stabilises across two consecutive samples or ‘attempts` polls have elapsed (whichever first). Returns the last rect read; never raises. Mirrors ferrum’s wait_for_stop_moving but no NodeMovingError because Lightpanda has no rendering loop, so a caller silently proceeding with the last rect is the right default.
63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/capybara/lightpanda/node.rb', line 63 def wait_for_stop_moving(delay: MOVING_WAIT_DELAY, attempts: MOVING_WAIT_ATTEMPTS) previous = rect attempts.times do sleep(delay) current = rect return current if current == previous previous = current end previous end |