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
-
#parents ⇒ Object
Ancestor chain from ‘parentNode` up to (but not including) `document`, returned as Lightpanda::Node wrappers.
- #path ⇒ Object
- #readonly? ⇒ Boolean
- #rect ⇒ Object
- #right_click(_keys = [], **_options) ⇒ Object
- #scroll_by ⇒ Object
-
#scroll_to ⇒ Object
Lightpanda has no rendering engine — ‘window.scrollTo` / `scrollIntoView` are no-ops at the browser level, and `getBoundingClientRect` reflects logical-DOM geometry rather than scroll-aware viewport coords.
- #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.
235 236 237 238 239 240 241 |
# File 'lib/capybara/lightpanda/node.rb', line 235 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.
87 88 89 |
# File 'lib/capybara/lightpanda/node.rb', line 87 def [](name) call(PROPERTY_OR_ATTRIBUTE_JS, name.to_s) end |
#all_text ⇒ Object
20 21 22 |
# File 'lib/capybara/lightpanda/node.rb', line 20 def all_text filter_text(call("function() { return this.textContent }")) end |
#backend_node_id ⇒ Object
253 254 255 256 257 |
# File 'lib/capybara/lightpanda/node.rb', line 253 def backend_node_id @backend_node_id ||= driver.browser.backend_node_id(@remote_object_id) rescue BrowserError nil end |
#checked? ⇒ Boolean
186 187 188 |
# File 'lib/capybara/lightpanda/node.rb', line 186 def checked? call("function() { return this.checked }") end |
#click(_keys = [], **_options) ⇒ Object
99 100 101 102 |
# File 'lib/capybara/lightpanda/node.rb', line 99 def click(_keys = [], **) call(CLICK_JS) driver.browser.wait_for_idle end |
#disabled? ⇒ Boolean
194 195 196 |
# File 'lib/capybara/lightpanda/node.rb', line 194 def disabled? call(DISABLED_JS) end |
#double_click(_keys = [], **_options) ⇒ Object
108 109 110 |
# File 'lib/capybara/lightpanda/node.rb', line 108 def double_click(_keys = [], **) call("function() { this.dispatchEvent(new MouseEvent('dblclick', {bubbles: true, cancelable: true})) }") end |
#find_css(selector) ⇒ Object
222 223 224 225 |
# File 'lib/capybara/lightpanda/node.rb', line 222 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
217 218 219 220 |
# File 'lib/capybara/lightpanda/node.rb', line 217 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).
249 250 251 |
# File 'lib/capybara/lightpanda/node.rb', line 249 def hash backend_node_id.hash end |
#hover ⇒ Object
112 113 114 |
# File 'lib/capybara/lightpanda/node.rb', line 112 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.
49 50 51 52 53 |
# File 'lib/capybara/lightpanda/node.rb', line 49 def moving?(delay: MOVING_WAIT_DELAY) previous = rect sleep(delay) previous != rect end |
#multiple? ⇒ Boolean
202 203 204 |
# File 'lib/capybara/lightpanda/node.rb', line 202 def multiple? call("function() { return this.multiple }") end |
#obscured? ⇒ Boolean
41 42 43 |
# File 'lib/capybara/lightpanda/node.rb', line 41 def obscured? call(OBSCURED_JS) end |
#parents ⇒ Object
Ancestor chain from ‘parentNode` up to (but not including) `document`, returned as Lightpanda::Node wrappers. Mirrors Cuprite’s ‘Node#parents`.
212 213 214 215 |
# File 'lib/capybara/lightpanda/node.rb', line 212 def parents oids = driver.browser.parents_of(@remote_object_id) oids.map { |oid| self.class.new(driver, oid) } end |
#path ⇒ Object
206 207 208 |
# File 'lib/capybara/lightpanda/node.rb', line 206 def path call(GET_PATH_JS) end |
#readonly? ⇒ Boolean
198 199 200 |
# File 'lib/capybara/lightpanda/node.rb', line 198 def readonly? call("function() { return this.readOnly }") end |
#rect ⇒ Object
37 38 39 |
# File 'lib/capybara/lightpanda/node.rb', line 37 def rect call(GET_RECT_JS) end |
#right_click(_keys = [], **_options) ⇒ Object
104 105 106 |
# File 'lib/capybara/lightpanda/node.rb', line 104 def right_click(_keys = [], **) call("function() { this.dispatchEvent(new MouseEvent('contextmenu', {bubbles: true, cancelable: true})) }") end |
#scroll_by ⇒ Object
125 |
# File 'lib/capybara/lightpanda/node.rb', line 125 def scroll_by(*); end |
#scroll_to ⇒ Object
Lightpanda has no rendering engine — ‘window.scrollTo` / `scrollIntoView` are no-ops at the browser level, and `getBoundingClientRect` reflects logical-DOM geometry rather than scroll-aware viewport coords. So there’s nothing to scroll. Silently succeed so callers like ‘session.scroll_to(find(’#thing’))‘ (Selenium-flavoured specs leaning on real layout) don’t crash with NotImplementedError; assertions that depend on post-scroll visibility are already gated by the cuprite fallback in dual-driver setups.
124 |
# File 'lib/capybara/lightpanda/node.rb', line 124 def scroll_to(*); end |
#select_option ⇒ Object
150 151 152 |
# File 'lib/capybara/lightpanda/node.rb', line 150 def select_option call(SELECT_OPTION_JS) end |
#selected? ⇒ Boolean
190 191 192 |
# File 'lib/capybara/lightpanda/node.rb', line 190 def selected? call("function() { return !!this.selected }") end |
#send_keys ⇒ Object
166 167 168 169 |
# File 'lib/capybara/lightpanda/node.rb', line 166 def send_keys(*) call("function() { this.focus() }") driver.browser.keyboard.type(*) end |
#set(value, **_options) ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/capybara/lightpanda/node.rb', line 135 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
72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/capybara/lightpanda/node.rb', line 72 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
95 96 97 |
# File 'lib/capybara/lightpanda/node.rb', line 95 def style(styles) styles.to_h { |style| [style, call(GET_STYLE_JS, style)] } end |
#tag_name ⇒ Object
171 172 173 174 175 176 177 178 179 180 |
# File 'lib/capybara/lightpanda/node.rb', line 171 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 |
# File 'lib/capybara/lightpanda/node.rb', line 16 def text 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’)‘).
131 132 133 |
# File 'lib/capybara/lightpanda/node.rb', line 131 def trigger(event) call(TRIGGER_JS, event.to_s) end |
#unselect_option ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/capybara/lightpanda/node.rb', line 154 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
91 92 93 |
# File 'lib/capybara/lightpanda/node.rb', line 91 def value call(GET_VALUE_JS) end |
#visible? ⇒ Boolean
182 183 184 |
# File 'lib/capybara/lightpanda/node.rb', line 182 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).
28 29 30 31 32 33 34 35 |
# File 'lib/capybara/lightpanda/node.rb', line 28 def visible_text 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.
60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/capybara/lightpanda/node.rb', line 60 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 |