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
- #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.
212 213 214 215 216 217 218 |
# File 'lib/capybara/lightpanda/node.rb', line 212 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
230 231 232 |
# File 'lib/capybara/lightpanda/node.rb', line 230 def backend_node_id @backend_node_id ||= driver.browser.backend_node_id(@remote_object_id) end |
#checked? ⇒ Boolean
170 171 172 |
# File 'lib/capybara/lightpanda/node.rb', line 170 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
178 179 180 |
# File 'lib/capybara/lightpanda/node.rb', line 178 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
199 200 201 202 |
# File 'lib/capybara/lightpanda/node.rb', line 199 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
194 195 196 197 |
# File 'lib/capybara/lightpanda/node.rb', line 194 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).
226 227 228 |
# File 'lib/capybara/lightpanda/node.rb', line 226 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
186 187 188 |
# File 'lib/capybara/lightpanda/node.rb', line 186 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
190 191 192 |
# File 'lib/capybara/lightpanda/node.rb', line 190 def path call(GET_PATH_JS) end |
#readonly? ⇒ Boolean
182 183 184 |
# File 'lib/capybara/lightpanda/node.rb', line 182 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
134 135 136 |
# File 'lib/capybara/lightpanda/node.rb', line 134 def select_option call(SELECT_OPTION_JS) end |
#selected? ⇒ Boolean
174 175 176 |
# File 'lib/capybara/lightpanda/node.rb', line 174 def selected? call("function() { return !!this.selected }") end |
#send_keys ⇒ Object
150 151 152 153 |
# File 'lib/capybara/lightpanda/node.rb', line 150 def send_keys(*) call("function() { this.focus() }") driver.browser.keyboard.type(*) end |
#set(value, **_options) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/capybara/lightpanda/node.rb', line 119 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
155 156 157 158 159 160 161 162 163 164 |
# File 'lib/capybara/lightpanda/node.rb', line 155 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 |
#unselect_option ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/capybara/lightpanda/node.rb', line 138 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
166 167 168 |
# File 'lib/capybara/lightpanda/node.rb', line 166 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 |