dommy 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -38
  3. data/lib/dommy/animation.rb +1 -1
  4. data/lib/dommy/attr.rb +23 -11
  5. data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
  6. data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
  7. data/lib/dommy/backend.rb +129 -0
  8. data/lib/dommy/blob.rb +2 -2
  9. data/lib/dommy/compression_streams.rb +4 -4
  10. data/lib/dommy/cookie_store.rb +1 -1
  11. data/lib/dommy/crypto.rb +9 -8
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +98 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +231 -50
  17. data/lib/dommy/event.rb +61 -25
  18. data/lib/dommy/event_source.rb +8 -8
  19. data/lib/dommy/fetch.rb +14 -6
  20. data/lib/dommy/file_reader.rb +3 -3
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +7 -4
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +110 -42
  25. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  26. data/lib/dommy/internal/dom_matching.rb +3 -3
  27. data/lib/dommy/internal/node_traversal.rb +1 -1
  28. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  29. data/lib/dommy/internal/template_content_registry.rb +6 -6
  30. data/lib/dommy/intersection_observer.rb +2 -2
  31. data/lib/dommy/location.rb +8 -4
  32. data/lib/dommy/media_query_list.rb +3 -3
  33. data/lib/dommy/message_channel.rb +9 -9
  34. data/lib/dommy/mutation_observer.rb +21 -11
  35. data/lib/dommy/navigator.rb +12 -12
  36. data/lib/dommy/node.rb +12 -0
  37. data/lib/dommy/notification.rb +3 -3
  38. data/lib/dommy/parser.rb +13 -13
  39. data/lib/dommy/performance_observer.rb +2 -2
  40. data/lib/dommy/range.rb +2 -2
  41. data/lib/dommy/resize_observer.rb +2 -2
  42. data/lib/dommy/shadow_root.rb +10 -8
  43. data/lib/dommy/streams.rb +22 -22
  44. data/lib/dommy/text_codec.rb +4 -4
  45. data/lib/dommy/tree_walker.rb +21 -21
  46. data/lib/dommy/url.rb +25 -8
  47. data/lib/dommy/version.rb +1 -1
  48. data/lib/dommy/web_socket.rb +13 -13
  49. data/lib/dommy/window.rb +14 -1
  50. data/lib/dommy/worker.rb +5 -5
  51. data/lib/dommy/xml_http_request.rb +19 -4
  52. data/lib/dommy.rb +12 -2
  53. metadata +12 -26
data/lib/dommy/element.rb CHANGED
@@ -9,7 +9,9 @@ module Dommy
9
9
  include EventTarget
10
10
  include Node
11
11
 
12
- attr_reader :__node__, :document
12
+ attr_reader :document
13
+
14
+ def __dommy_backend_node__ = @__node__
13
15
 
14
16
  def initialize(document, nokogiri_node)
15
17
  @document = document
@@ -98,6 +100,12 @@ module Dommy
98
100
  end
99
101
  end
100
102
 
103
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
104
+ JS_METHOD_NAMES = %w[cloneNode querySelector querySelectorAll getElementById appendChild].freeze
105
+ def __js_method_names__
106
+ JS_METHOD_NAMES
107
+ end
108
+
101
109
  def __js_call__(method, args)
102
110
  case method
103
111
  when "cloneNode"
@@ -128,9 +136,9 @@ module Dommy
128
136
  def detach_dom_nodes(value)
129
137
  case value
130
138
  when String
131
- [@document.create_text_node(value).__node__]
139
+ [@document.create_text_node(value).__dommy_backend_node__]
132
140
  else
133
- node = value.respond_to?(:__node__) ? value.__node__ : nil
141
+ node = value.respond_to?(:__dommy_backend_node__) ? value.__dommy_backend_node__ : nil
134
142
  return [] unless node
135
143
 
136
144
  node.unlink if node.parent
@@ -147,7 +155,7 @@ module Dommy
147
155
 
148
156
  # Fragments aren't part of the bubble chain; nil terminates
149
157
  # bubbling at the boundary (shadow root, detached fragment, etc.).
150
- def __event_parent__
158
+ def __internal_event_parent__
151
159
  nil
152
160
  end
153
161
  end
@@ -157,7 +165,7 @@ module Dommy
157
165
  class CharacterDataNode
158
166
  include Node
159
167
 
160
- attr_reader :__node__
168
+ def __dommy_backend_node__ = @__node__
161
169
 
162
170
  def initialize(document, nokogiri_node)
163
171
  @document = document
@@ -191,7 +199,19 @@ module Dommy
191
199
  end
192
200
 
193
201
  def remove
202
+ parent = @__node__.parent
203
+ removed = @__node__
194
204
  @__node__.unlink
205
+ # Mirror Element#remove_child: notify with the Nokogiri::Node
206
+ # (not the Dommy wrapper) so MutationCoordinator's wrap_node
207
+ # cache keys consistently.
208
+ if parent
209
+ @document.notify_child_list_mutation(
210
+ target_node: parent,
211
+ added_nodes: [],
212
+ removed_nodes: [removed]
213
+ )
214
+ end
195
215
  nil
196
216
  end
197
217
 
@@ -243,16 +263,105 @@ module Dommy
243
263
  nil
244
264
  end
245
265
 
246
- def __js_call__(method, _args)
266
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
267
+ JS_METHOD_NAMES = %w[remove before after replaceWith].freeze
268
+ def __js_method_names__
269
+ JS_METHOD_NAMES
270
+ end
271
+
272
+ def __js_call__(method, args)
247
273
  case method
248
274
  when "remove"
249
- @__node__.unlink
250
- nil
275
+ remove
276
+ when "before"
277
+ before(*args)
278
+ when "after"
279
+ after(*args)
280
+ when "replaceWith"
281
+ replace_with(*args)
282
+ end
283
+ end
284
+
285
+ # ChildNode mixin: WHATWG DOM defines `before`, `after`,
286
+ # `replaceWith` on all child nodes, including Text and Comment.
287
+ # Implementations operate on the Nokogiri layer and notify the
288
+ # MutationObserver with the underlying nodes (mirroring
289
+ # Element#remove_child / replace_child).
290
+
291
+ def before(*args)
292
+ parent = @__node__.parent
293
+ return nil unless parent
294
+
295
+ added = args.map { |arg| coerce_node(arg) }.compact
296
+ added.reverse_each { |node| @__node__.add_previous_sibling(node) }
297
+ notify_child_list_added(parent, added)
298
+ nil
299
+ end
300
+
301
+ def after(*args)
302
+ parent = @__node__.parent
303
+ return nil unless parent
304
+
305
+ added = args.map { |arg| coerce_node(arg) }.compact
306
+ anchor = @__node__.next_sibling
307
+ if anchor
308
+ added.reverse_each { |node| anchor.add_previous_sibling(node) }
309
+ else
310
+ added.each { |node| parent.add_child(node) }
311
+ end
312
+ notify_child_list_added(parent, added)
313
+ nil
314
+ end
315
+
316
+ def replace_with(*args)
317
+ parent = @__node__.parent
318
+ return nil unless parent
319
+
320
+ added = args.map { |arg| coerce_node(arg) }.compact
321
+ removed = @__node__
322
+ anchor = @__node__.next_sibling
323
+ @__node__.unlink
324
+ if anchor
325
+ added.reverse_each { |node| anchor.add_previous_sibling(node) }
326
+ else
327
+ added.each { |node| parent.add_child(node) }
251
328
  end
329
+ @document.notify_child_list_mutation(
330
+ target_node: parent,
331
+ added_nodes: added,
332
+ removed_nodes: [removed]
333
+ )
334
+ nil
252
335
  end
253
336
 
254
337
  private
255
338
 
339
+ # Coerce a `before` / `after` / `replaceWith` argument into a raw
340
+ # Nokogiri node, ready to be linked into a parent. Strings become
341
+ # fresh text nodes; existing nodes are detached from their current
342
+ # parent first (matching Element#detach_dom_nodes minus the
343
+ # Fragment branch which is rarely needed off a text/comment node).
344
+ def coerce_node(arg)
345
+ case arg
346
+ when String
347
+ @document.create_text_node(arg).__dommy_backend_node__
348
+ else
349
+ node = arg.respond_to?(:__dommy_backend_node__) ? arg.__dommy_backend_node__ : nil
350
+ node.unlink if node && node.parent
351
+ node
352
+ end
353
+ end
354
+
355
+ def notify_child_list_added(parent, added)
356
+ return if added.empty?
357
+
358
+ @document.notify_child_list_mutation(
359
+ target_node: parent,
360
+ added_nodes: added,
361
+ removed_nodes: []
362
+ )
363
+ end
364
+
256
365
  def write_data(value)
257
366
  old = @__node__.content
258
367
  @__node__.content = value.to_s
@@ -265,6 +374,12 @@ module Dommy
265
374
  3
266
375
  end
267
376
 
377
+ # Own __js_call__ methods, on top of CharacterDataNode's.
378
+ JS_METHOD_NAMES = %w[cloneNode].freeze
379
+ def __js_method_names__
380
+ super + JS_METHOD_NAMES
381
+ end
382
+
268
383
  def __js_call__(method, args)
269
384
  case method
270
385
  when "cloneNode"
@@ -280,6 +395,12 @@ module Dommy
280
395
  8
281
396
  end
282
397
 
398
+ # Own __js_call__ methods, on top of CharacterDataNode's.
399
+ JS_METHOD_NAMES = %w[cloneNode].freeze
400
+ def __js_method_names__
401
+ super + JS_METHOD_NAMES
402
+ end
403
+
283
404
  def __js_call__(method, args)
284
405
  case method
285
406
  when "cloneNode"
@@ -311,7 +432,7 @@ module Dommy
311
432
  end
312
433
 
313
434
  def value
314
- @element.__node__["class"].to_s
435
+ @element.__dommy_backend_node__["class"].to_s
315
436
  end
316
437
 
317
438
  def value=(new_value)
@@ -383,6 +504,12 @@ module Dommy
383
504
  nil
384
505
  end
385
506
 
507
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
508
+ JS_METHOD_NAMES = %w[add remove contains toggle replace item].freeze
509
+ def __js_method_names__
510
+ JS_METHOD_NAMES
511
+ end
512
+
386
513
  def __js_call__(method, args)
387
514
  case method
388
515
  when "add"
@@ -444,14 +571,14 @@ module Dommy
444
571
  end
445
572
 
446
573
  def class_tokens
447
- raw = @element.__node__["class"].to_s
574
+ raw = @element.__dommy_backend_node__["class"].to_s
448
575
  raw.split(/\s+/).reject(&:empty?)
449
576
  end
450
577
 
451
578
  def update_tokens
452
579
  tokens = yield(class_tokens)
453
580
  if tokens.empty?
454
- @element.remove_attribute("class") if @element.__node__.key?("class")
581
+ @element.remove_attribute("class") if @element.__dommy_backend_node__.key?("class")
455
582
  else
456
583
  @element.set_attribute("class", tokens.join(" "))
457
584
  end
@@ -467,7 +594,7 @@ module Dommy
467
594
  end
468
595
 
469
596
  def __js_get__(key)
470
- @element.__node__[attr_name(key)]
597
+ @element.__dommy_backend_node__[attr_name(key)]
471
598
  end
472
599
 
473
600
  def __js_set__(key, value)
@@ -629,6 +756,12 @@ module Dommy
629
756
  nil
630
757
  end
631
758
 
759
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
760
+ JS_METHOD_NAMES = %w[setProperty removeProperty getPropertyValue item].freeze
761
+ def __js_method_names__
762
+ JS_METHOD_NAMES
763
+ end
764
+
632
765
  def __js_call__(method, args)
633
766
  case method
634
767
  when "setProperty"
@@ -674,7 +807,7 @@ module Dommy
674
807
  end
675
808
 
676
809
  def properties
677
- raw = @element.__node__["style"].to_s
810
+ raw = @element.__dommy_backend_node__["style"].to_s
678
811
  raw.split(";").each_with_object({}) do |entry, out|
679
812
  key, value = entry.split(":", 2)
680
813
  next unless key && value
@@ -685,7 +818,7 @@ module Dommy
685
818
 
686
819
  def write_properties(props)
687
820
  if props.empty?
688
- @element.remove_attribute("style") if @element.__node__.key?("style")
821
+ @element.remove_attribute("style") if @element.__dommy_backend_node__.key?("style")
689
822
  else
690
823
  @element.set_attribute("style", props.map { |k, v| "#{k}:#{v}" }.join(";"))
691
824
  end
@@ -696,7 +829,9 @@ module Dommy
696
829
  include EventTarget
697
830
  include Node
698
831
 
699
- attr_reader :__node__, :document
832
+ attr_reader :document
833
+
834
+ def __dommy_backend_node__ = @__node__
700
835
 
701
836
  def initialize(document, nokogiri_node)
702
837
  @document = document
@@ -860,7 +995,7 @@ module Dommy
860
995
  parent = @__node__.parent
861
996
  return unless parent
862
997
 
863
- if parent.is_a?(Nokogiri::XML::Document)
998
+ if parent.is_a?(Backend.document_class)
864
999
  raise(
865
1000
  DOMException::NoModificationAllowedError,
866
1001
  "outerHTML setter not allowed on the document element"
@@ -884,9 +1019,9 @@ module Dommy
884
1019
  # `el.contains(other)` — true if `other` is `el` itself or any
885
1020
  # descendant. Per spec, returns false for null/non-Node.
886
1021
  def contains?(other)
887
- return false unless other.respond_to?(:__node__)
1022
+ return false unless other.respond_to?(:__dommy_backend_node__)
888
1023
 
889
- other_node = other.__node__
1024
+ other_node = other.__dommy_backend_node__
890
1025
  return true if other_node == @__node__
891
1026
 
892
1027
  Internal::NodeTraversal.ancestor_of?(@__node__, other_node)
@@ -897,7 +1032,7 @@ module Dommy
897
1032
  # inside a shadow tree, returns that ShadowRoot. Otherwise walks
898
1033
  # until we hit the Nokogiri Document (then returns the Document).
899
1034
  def root_node
900
- sr = @document.__shadow_root_containing__(@__node__)
1035
+ sr = @document.__internal_shadow_root_containing__(@__node__)
901
1036
  return sr if sr
902
1037
 
903
1038
  current = @__node__
@@ -905,7 +1040,7 @@ module Dommy
905
1040
  loop do
906
1041
  parent = current.respond_to?(:parent) ? current.parent : nil
907
1042
  break unless parent
908
- if parent.is_a?(Nokogiri::XML::Document)
1043
+ if parent.is_a?(Backend.document_class)
909
1044
  attached = true
910
1045
  break
911
1046
  end
@@ -1054,14 +1189,14 @@ module Dommy
1054
1189
 
1055
1190
  parent = current.respond_to?(:parent) ? current.parent : nil
1056
1191
  return false unless parent
1057
- return true if parent.is_a?(Nokogiri::XML::Document)
1192
+ return true if parent.is_a?(Backend.document_class)
1058
1193
 
1059
- sr = @document.__shadow_root_for_fragment__(parent)
1194
+ sr = @document.__internal_shadow_root_for_fragment__(parent)
1060
1195
  if sr
1061
1196
  host = sr.host
1062
1197
  return false unless host
1063
1198
 
1064
- current = host.__node__
1199
+ current = host.__dommy_backend_node__
1065
1200
  else
1066
1201
  current = parent
1067
1202
  end
@@ -1074,12 +1209,12 @@ module Dommy
1074
1209
  # tests rely on `document.activeElement` updating. Track the most
1075
1210
  # recently focused element on the document.
1076
1211
  def focus
1077
- @document.__set_active_element__(self)
1212
+ @document.__internal_set_active_element__(self)
1078
1213
  nil
1079
1214
  end
1080
1215
 
1081
1216
  def blur
1082
- @document.__set_active_element__(nil)
1217
+ @document.__internal_set_active_element__(nil)
1083
1218
  nil
1084
1219
  end
1085
1220
 
@@ -1149,7 +1284,7 @@ module Dommy
1149
1284
 
1150
1285
  # Internal — gives access to the shadow root regardless of mode.
1151
1286
  # Used by event composition / `composedPath()`.
1152
- def __shadow_root__
1287
+ def __internal_shadow_root__
1153
1288
  @__shadow_root
1154
1289
  end
1155
1290
 
@@ -1157,7 +1292,7 @@ module Dommy
1157
1292
  # "beforebegin", "afterbegin", "beforeend", "afterend". Returns the
1158
1293
  # inserted element or nil if position has no anchor (root cases).
1159
1294
  def insert_adjacent_element(position, element)
1160
- return nil unless element.respond_to?(:__node__)
1295
+ return nil unless element.respond_to?(:__dommy_backend_node__)
1161
1296
 
1162
1297
  case position.to_s
1163
1298
  when "beforebegin"
@@ -1256,10 +1391,10 @@ module Dommy
1256
1391
  # nodes).
1257
1392
  def compare_document_position(other)
1258
1393
  return 0 if equal?(other)
1259
- return DOCUMENT_POSITION_DISCONNECTED unless other.respond_to?(:__node__)
1394
+ return DOCUMENT_POSITION_DISCONNECTED unless other.respond_to?(:__dommy_backend_node__)
1260
1395
 
1261
1396
  self_node = @__node__
1262
- other_node = other.__node__
1397
+ other_node = other.__dommy_backend_node__
1263
1398
 
1264
1399
  self_ancestors = ancestor_chain(self_node)
1265
1400
  other_ancestors = ancestor_chain(other_node)
@@ -1309,11 +1444,11 @@ module Dommy
1309
1444
  # suite and standard DOM Node.isEqualNode.
1310
1445
  def equal_node?(other)
1311
1446
  return false unless other.is_a?(Element)
1312
- return false unless @__node__.name == other.__node__.name
1447
+ return false unless @__node__.name == other.__dommy_backend_node__.name
1313
1448
  return false unless attribute_signature == other.send(:attribute_signature)
1314
- return false unless @__node__.children.size == other.__node__.children.size
1449
+ return false unless @__node__.children.size == other.__dommy_backend_node__.children.size
1315
1450
 
1316
- @__node__.children.zip(other.__node__.children).all? do |a, b|
1451
+ @__node__.children.zip(other.__dommy_backend_node__.children).all? do |a, b|
1317
1452
  wa = @document.wrap_node(a)
1318
1453
  wb = @document.wrap_node(b)
1319
1454
  wa.respond_to?(:equal_node?) ? wa.equal_node?(wb) : a.content == b.content
@@ -1541,7 +1676,20 @@ module Dommy
1541
1676
  def __js_set__(key, value)
1542
1677
  case key
1543
1678
  when "textContent"
1679
+ # `node.content =` removes all existing children and (if
1680
+ # value is non-empty) appends a single text node. Capture
1681
+ # before/after to feed MutationObserver — mirrors the
1682
+ # innerHTML branch below.
1683
+ removed = @__node__.children.to_a
1544
1684
  @__node__.content = value.to_s
1685
+ added = @__node__.children.to_a
1686
+ if removed.any? || added.any?
1687
+ @document.notify_child_list_mutation(
1688
+ target_node: @__node__,
1689
+ added_nodes: added,
1690
+ removed_nodes: removed
1691
+ )
1692
+ end
1545
1693
  when "innerHTML"
1546
1694
  removed = @__node__.children.to_a
1547
1695
  if @__node__.name == "template"
@@ -1611,6 +1759,21 @@ module Dommy
1611
1759
 
1612
1760
  public
1613
1761
 
1762
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
1763
+ JS_METHOD_NAMES = %w[
1764
+ getAttribute setAttribute hasAttribute removeAttribute getAttributeNames closest
1765
+ querySelector querySelectorAll getElementsByClassName getElementsByTagName
1766
+ insertAdjacentElement insertAdjacentHTML insertAdjacentText toggleAttribute matches
1767
+ toString getAttributeNode setAttributeNode removeAttributeNode focus blur attachShadow
1768
+ addEventListener removeEventListener dispatchEvent appendChild insertBefore removeChild
1769
+ replaceChild cloneNode append prepend replaceChildren before after getInnerHTML getHTML
1770
+ remove replaceWith click getBoundingClientRect getClientRects scrollIntoView scroll
1771
+ scrollTo scrollBy requestFullscreen showPopover hidePopover togglePopover
1772
+ ].freeze
1773
+ def __js_method_names__
1774
+ JS_METHOD_NAMES
1775
+ end
1776
+
1614
1777
  def __js_call__(method, args)
1615
1778
  case method
1616
1779
  when "getAttribute"
@@ -1700,11 +1863,11 @@ module Dommy
1700
1863
  []
1701
1864
  when "scrollIntoView", "scroll", "scrollTo", "scrollBy"
1702
1865
  # No layout — record the request for tests to assert against.
1703
- @__scroll_log__ ||= []
1704
- @__scroll_log__ << [method, args]
1866
+ @scroll_log ||= []
1867
+ @scroll_log << [method, args]
1705
1868
  nil
1706
1869
  when "requestFullscreen"
1707
- @document.__set_fullscreen_element__(self)
1870
+ @document.__internal_set_fullscreen_element__(self)
1708
1871
  PromiseValue.resolve(@document.default_view, nil)
1709
1872
  when "showPopover"
1710
1873
  toggle_popover_state(true)
@@ -1739,14 +1902,14 @@ module Dommy
1739
1902
  @document.wrap_node(node)
1740
1903
  end
1741
1904
 
1742
- def __event_parent__
1905
+ def __internal_event_parent__
1743
1906
  parent_node = @__node__.parent
1744
1907
  # If our Nokogiri parent is a shadow tree's backing fragment,
1745
1908
  # the bubble path's next stop is the ShadowRoot itself — not
1746
- # the bare Fragment wrapper. The ShadowRoot's __event_parent__
1909
+ # the bare Fragment wrapper. The ShadowRoot's __internal_event_parent__
1747
1910
  # will return nil (composed events route to host explicitly).
1748
- if parent_node.is_a?(Nokogiri::XML::DocumentFragment)
1749
- sr = @document.__shadow_root_for_fragment__(parent_node)
1911
+ if parent_node.is_a?(Backend.document_fragment_class)
1912
+ sr = @document.__internal_shadow_root_for_fragment__(parent_node)
1750
1913
  return sr if sr
1751
1914
  end
1752
1915
 
@@ -1841,13 +2004,28 @@ module Dommy
1841
2004
  def query_selector(selector)
1842
2005
  return nil if selector.nil? || selector.to_s.empty?
1843
2006
 
1844
- @document.wrap_node(@__node__.at_css(selector.to_s))
2007
+ @document.wrap_node(@__node__.at_css(selector.to_s, Internal::CSS_PSEUDO_HANDLERS))
1845
2008
  end
1846
2009
 
1847
2010
  def query_selector_all(selector)
1848
2011
  return NodeList.new if selector.nil? || selector.to_s.empty?
1849
2012
 
1850
- NodeList.new(@__node__.css(selector.to_s).map { |node| @document.wrap_node(node) }.compact)
2013
+ NodeList.new(@__node__.css(selector.to_s, Internal::CSS_PSEUDO_HANDLERS).map { |node| @document.wrap_node(node) }.compact)
2014
+ end
2015
+
2016
+ # XPath queries scoped to this element, returning wrapped nodes.
2017
+ def at_xpath(expression)
2018
+ node = @__node__.at_xpath(expression)
2019
+ node && @document.wrap_node(node)
2020
+ end
2021
+
2022
+ def xpath(expression)
2023
+ @__node__.xpath(expression).map { |node| @document.wrap_node(node) }
2024
+ end
2025
+
2026
+ # The XPath string locating this element in its document.
2027
+ def path
2028
+ @__node__.path
1851
2029
  end
1852
2030
 
1853
2031
  def append_child(child)
@@ -1993,10 +2171,10 @@ module Dommy
1993
2171
  # produce a cycle (inserting an ancestor as a descendant of
1994
2172
  # itself). Strings and Fragments are always safe.
1995
2173
  def check_hierarchy!(child)
1996
- return unless child.respond_to?(:__node__)
2174
+ return unless child.respond_to?(:__dommy_backend_node__)
1997
2175
 
1998
- node = child.__node__
1999
- return unless node.is_a?(Nokogiri::XML::Node)
2176
+ node = child.__dommy_backend_node__
2177
+ return unless node.is_a?(Backend.node_class)
2000
2178
 
2001
2179
  if node == @__node__ || @__node__.ancestors.any? { |a| a == node }
2002
2180
  raise(
@@ -2013,13 +2191,13 @@ module Dommy
2013
2191
  def detach_dom_nodes(value)
2014
2192
  case value
2015
2193
  when Element, TextNode, CommentNode
2016
- node = value.__node__
2194
+ node = value.__dommy_backend_node__
2017
2195
  node.unlink if node.parent
2018
2196
  [node]
2019
2197
  when Fragment
2020
2198
  value.extract_children
2021
2199
  when String
2022
- [@document.create_text_node(value).__node__]
2200
+ [@document.create_text_node(value).__dommy_backend_node__]
2023
2201
  else
2024
2202
  node = unwrap_dom_node(value)
2025
2203
  return [] unless node
@@ -2030,7 +2208,7 @@ module Dommy
2030
2208
  end
2031
2209
 
2032
2210
  def unwrap_dom_node(value)
2033
- return value.__node__ if value.respond_to?(:__node__)
2211
+ return value.__dommy_backend_node__ if value.respond_to?(:__dommy_backend_node__)
2034
2212
 
2035
2213
  nil
2036
2214
  end
@@ -2066,11 +2244,11 @@ module Dommy
2066
2244
  end
2067
2245
 
2068
2246
  # Test inspector for scroll calls (no real layout to scroll).
2069
- def __scroll_log__
2070
- @__scroll_log__ ||= []
2247
+ def __test_scroll_log__
2248
+ @scroll_log ||= []
2071
2249
  end
2072
2250
 
2073
- public :__scroll_log__
2251
+ public :__test_scroll_log__
2074
2252
 
2075
2253
  # Re-expose snake_case methods that the JS bridge dispatch routes
2076
2254
  # to. Defined as private originally so internal helpers (element_children,
@@ -2088,6 +2266,9 @@ module Dommy
2088
2266
  :clone_node,
2089
2267
  :query_selector,
2090
2268
  :query_selector_all,
2269
+ :at_xpath,
2270
+ :xpath,
2271
+ :path,
2091
2272
  :closest,
2092
2273
  :animate,
2093
2274
  :get_animations,