dommy 0.5.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +213 -0
  3. data/lib/dommy/attr.rb +200 -0
  4. data/lib/dommy/blob.rb +182 -0
  5. data/lib/dommy/bridge.rb +141 -0
  6. data/lib/dommy/css.rb +283 -0
  7. data/lib/dommy/custom_elements.rb +125 -0
  8. data/lib/dommy/data_transfer.rb +98 -0
  9. data/lib/dommy/document.rb +674 -0
  10. data/lib/dommy/dom_exception.rb +258 -0
  11. data/lib/dommy/dom_parser.rb +88 -0
  12. data/lib/dommy/element.rb +1975 -0
  13. data/lib/dommy/event.rb +589 -0
  14. data/lib/dommy/fetch.rb +241 -0
  15. data/lib/dommy/form_data.rb +208 -0
  16. data/lib/dommy/html_collection.rb +207 -0
  17. data/lib/dommy/html_elements.rb +4455 -0
  18. data/lib/dommy/internal/cookie_jar.rb +27 -0
  19. data/lib/dommy/internal/dom_matching.rb +141 -0
  20. data/lib/dommy/internal/mutation_coordinator.rb +172 -0
  21. data/lib/dommy/internal/node_traversal.rb +36 -0
  22. data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
  23. data/lib/dommy/internal/observer_manager.rb +31 -0
  24. data/lib/dommy/internal/observer_matcher.rb +31 -0
  25. data/lib/dommy/internal/scope_resolution.rb +27 -0
  26. data/lib/dommy/internal/shadow_root_registry.rb +35 -0
  27. data/lib/dommy/internal/template_content_registry.rb +97 -0
  28. data/lib/dommy/minitest/assertions.rb +105 -0
  29. data/lib/dommy/minitest.rb +17 -0
  30. data/lib/dommy/navigator.rb +271 -0
  31. data/lib/dommy/node.rb +218 -0
  32. data/lib/dommy/observer.rb +199 -0
  33. data/lib/dommy/parser.rb +29 -0
  34. data/lib/dommy/promise.rb +199 -0
  35. data/lib/dommy/router.rb +275 -0
  36. data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
  37. data/lib/dommy/rspec/matchers.rb +230 -0
  38. data/lib/dommy/rspec.rb +18 -0
  39. data/lib/dommy/scheduler.rb +135 -0
  40. data/lib/dommy/shadow_root.rb +255 -0
  41. data/lib/dommy/storage.rb +112 -0
  42. data/lib/dommy/test_helpers.rb +78 -0
  43. data/lib/dommy/tree_walker.rb +425 -0
  44. data/lib/dommy/url.rb +479 -0
  45. data/lib/dommy/version.rb +5 -0
  46. data/lib/dommy/world.rb +209 -0
  47. data/lib/dommy.rb +119 -0
  48. metadata +110 -0
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Internal
5
+ # Manages <template> element content fragments.
6
+ # When HTML contains <template>X</template>, the inner content X is
7
+ # detached and stored in a separate DocumentFragment, accessed via
8
+ # the element's `content` property (per HTML spec).
9
+ #
10
+ # Keeping these fragments off-document is what makes template content
11
+ # invisible to querySelector, getElementById, etc., on the main tree.
12
+ class TemplateContentRegistry
13
+ def initialize(document)
14
+ @document = document
15
+ # template_node.object_id → Nokogiri fragment
16
+ @fragments = {}
17
+ end
18
+
19
+ # Parse HTML into a fragment and attach it as the template's content.
20
+ # Drops any pre-existing direct children of the template element.
21
+ def attach(template_element, html)
22
+ template_element.__node__.children.each(&:unlink)
23
+ fragment = @document.nokogiri_doc.fragment(html.to_s)
24
+ @fragments[template_element.__node__.object_id] = fragment
25
+ fragment
26
+ end
27
+
28
+ # Get the wrapped Fragment for a template element, seeding from
29
+ # the template's current children if not previously migrated.
30
+ def fragment_for(template_element)
31
+ fragment = @fragments[template_element.__node__.object_id]
32
+ fragment ||= seed(template_element)
33
+ @document.wrap_node(fragment)
34
+ end
35
+
36
+ # Raw (Nokogiri) fragment lookup by Nokogiri node — used by
37
+ # internal traversal to skip template-content sub-trees.
38
+ def raw_fragment_for(nokogiri_node)
39
+ @fragments[nokogiri_node.object_id]
40
+ end
41
+
42
+ def inner_html_of(template_element)
43
+ fragment = @fragments[template_element.__node__.object_id]
44
+ return "" unless fragment
45
+
46
+ fragment.children.map(&:to_html).join
47
+ end
48
+
49
+ def has_content?(nokogiri_node)
50
+ @fragments.key?(nokogiri_node.object_id)
51
+ end
52
+
53
+ # Direct register — called after manual fragment construction
54
+ # (e.g., when seeding from existing template children).
55
+ def store(template_node, fragment)
56
+ @fragments[template_node.object_id] = fragment
57
+ end
58
+
59
+ # Walk a Nokogiri subtree, finding <template> elements whose
60
+ # children are still direct (not yet migrated to a fragment), and
61
+ # migrate each one. Called after innerHTML / fragment-parsing to
62
+ # keep template content out of the main tree.
63
+ def migrate_descendants(root)
64
+ targets = []
65
+ targets << root if template_needing_migration?(root)
66
+ root.traverse do |node|
67
+ targets << node if template_needing_migration?(node)
68
+ end
69
+
70
+ targets.uniq.each { |t| migrate_one(t) }
71
+ end
72
+
73
+ private
74
+
75
+ def template_needing_migration?(node)
76
+ return false unless node.respond_to?(:name) && node.name == "template"
77
+
78
+ !has_content?(node)
79
+ end
80
+
81
+ def seed(template_element)
82
+ migrate_one(template_element.__node__)
83
+ @fragments[template_element.__node__.object_id]
84
+ end
85
+
86
+ def migrate_one(template_node)
87
+ fragment = @document.nokogiri_doc.fragment("")
88
+ template_node.children.to_a.each do |child|
89
+ child.unlink
90
+ fragment.add_child(child)
91
+ end
92
+
93
+ @fragments[template_node.object_id] = fragment
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internal/dom_matching"
4
+ require_relative "../internal/scope_resolution"
5
+
6
+ module Dommy
7
+ module Minitest
8
+ # Custom Minitest assertions for testing Dommy DOM objects.
9
+ #
10
+ # @example
11
+ # require "dommy/minitest"
12
+ #
13
+ # class MyTest < Minitest::Test
14
+ # include Dommy::Minitest::Assertions
15
+ #
16
+ # def test_renders_button
17
+ # dom = parse_html("<button class='primary'>Submit</button>")
18
+ # assert_dom_contains(dom, "button.primary")
19
+ # assert_dom_contains_text(dom, "Submit")
20
+ # end
21
+ # end
22
+ module Assertions
23
+ def assert_dom_contains(scope, selector, text: nil, count: nil, msg: nil)
24
+ matched = dom_matched_for(scope, selector, text: text)
25
+ msg ||= "expected to contain DOM matching #{selector.inspect}" \
26
+ "#{text ? " with text #{text.inspect}" : ""}" \
27
+ "#{count ? " (count: #{count.inspect})" : ""}, found #{matched.size}"
28
+ assert(Internal::DomMatching.count_matches?(matched.size, count), msg)
29
+ end
30
+
31
+ def refute_dom_contains(scope, selector, text: nil, count: nil, msg: nil)
32
+ matched = dom_matched_for(scope, selector, text: text)
33
+ msg ||= "expected NOT to contain DOM matching #{selector.inspect}" \
34
+ "#{text ? " with text #{text.inspect}" : ""}, found #{matched.size}"
35
+ refute(Internal::DomMatching.count_matches?(matched.size, count), msg)
36
+ end
37
+
38
+ def assert_dom_contains_text(scope, text, msg: nil)
39
+ actual = dom_text_of(scope)
40
+ msg ||= "expected text to include #{text.inspect}, got #{actual.inspect}"
41
+ assert(Internal::DomMatching.text_matches?(actual, text), msg)
42
+ end
43
+
44
+ def refute_dom_contains_text(scope, text, msg: nil)
45
+ actual = dom_text_of(scope)
46
+ msg ||= "expected text NOT to include #{text.inspect}, got #{actual.inspect}"
47
+ refute(Internal::DomMatching.text_matches?(actual, text), msg)
48
+ end
49
+
50
+ # Without a value argument, checks attribute existence only.
51
+ # With a value, checks string equality.
52
+ def assert_dom_has_attribute(element, name, value = UNSET, msg: nil)
53
+ present = element.has_attribute?(name.to_s)
54
+ if value.equal?(UNSET)
55
+ msg ||= "expected element to have attribute #{name.inspect}"
56
+ assert(present, msg)
57
+ else
58
+ actual = element.get_attribute(name.to_s)
59
+ msg ||= "expected attribute #{name.inspect} to equal #{value.inspect}, got #{actual.inspect}"
60
+ assert_equal(value.to_s, actual.to_s, msg)
61
+ end
62
+ end
63
+
64
+ def refute_dom_has_attribute(element, name, msg: nil)
65
+ present = element.has_attribute?(name.to_s)
66
+ msg ||= "expected element NOT to have attribute #{name.inspect}"
67
+ refute(present, msg)
68
+ end
69
+
70
+ def assert_dom_has_class(element, class_name, msg: nil)
71
+ actual_classes = element.class_list.value.to_s.split(/\s+/)
72
+ msg ||= "expected element to have class #{class_name.inspect}, got #{actual_classes.inspect}"
73
+ assert_includes(actual_classes, class_name.to_s, msg)
74
+ end
75
+
76
+ def refute_dom_has_class(element, class_name, msg: nil)
77
+ actual_classes = element.class_list.value.to_s.split(/\s+/)
78
+ msg ||= "expected element NOT to have class #{class_name.inspect}, got #{actual_classes.inspect}"
79
+ refute_includes(actual_classes, class_name.to_s, msg)
80
+ end
81
+
82
+ def assert_dom_html_equal(scope, expected_html, msg: nil)
83
+ scope = Internal::ScopeResolution.resolve(scope)
84
+ actual_n = Internal::DomMatching.normalize_html(Internal::DomMatching.html_of(scope))
85
+ expected_n = Internal::DomMatching.normalize_html(expected_html)
86
+ msg ||= "expected DOM HTML to match.\nExpected: #{expected_n}\nActual: #{actual_n}"
87
+ assert_equal(expected_n, actual_n, msg)
88
+ end
89
+
90
+ # Sentinel for "value was not passed"
91
+ UNSET = Object.new.freeze
92
+ private_constant :UNSET
93
+
94
+ private
95
+
96
+ def dom_matched_for(scope, selector, text:)
97
+ Internal::DomMatching.filter(Internal::ScopeResolution.resolve(scope), selector, text: text)
98
+ end
99
+
100
+ def dom_text_of(scope)
101
+ Internal::DomMatching.text_of(Internal::ScopeResolution.resolve(scope))
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point for using Dommy from Minitest test suites.
4
+ # Loads the test helpers and DOM assertion modules so users can
5
+ # `include` them into their test classes.
6
+ #
7
+ # @example
8
+ # require "dommy/minitest"
9
+ #
10
+ # class MyTest < Minitest::Test
11
+ # include Dommy::TestHelpers
12
+ # include Dommy::Minitest::Assertions
13
+ # end
14
+
15
+ require "dommy"
16
+ require "dommy/test_helpers"
17
+ require "dommy/minitest/assertions"
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `window.navigator` — exposes browser-agent metadata plus
5
+ # `clipboard` / `permissions` sub-objects. Dommy returns sensible
6
+ # defaults (Dommy as user agent, "en" language, online=true) that
7
+ # tests can override.
8
+ class Navigator
9
+ DEFAULT_USER_AGENT = "Mozilla/5.0 (Dommy) Ruby"
10
+
11
+ attr_accessor :user_agent, :language, :languages, :platform, :vendor, :on_line, :cookie_enabled
12
+
13
+ def initialize(window)
14
+ @window = window
15
+ @user_agent = DEFAULT_USER_AGENT
16
+ @language = "en"
17
+ @languages = ["en"].freeze
18
+ @platform = "Dommy"
19
+ @vendor = "Dommy"
20
+ @on_line = true
21
+ @cookie_enabled = true
22
+ @clipboard = Clipboard.new(window)
23
+ @permissions = Permissions.new(window)
24
+ end
25
+
26
+ attr_reader :clipboard, :permissions
27
+
28
+ def [](key)
29
+ __js_get__(key.to_s)
30
+ end
31
+
32
+ def []=(k, v)
33
+ __js_set__(k.to_s, v)
34
+ end
35
+
36
+ def __js_get__(key)
37
+ case key
38
+ when "userAgent"
39
+ @user_agent
40
+ when "language"
41
+ @language
42
+ when "languages"
43
+ @languages
44
+ when "platform"
45
+ @platform
46
+ when "vendor"
47
+ @vendor
48
+ when "onLine"
49
+ @on_line
50
+ when "cookieEnabled"
51
+ @cookie_enabled
52
+ when "clipboard"
53
+ @clipboard
54
+ when "permissions"
55
+ @permissions
56
+ end
57
+ end
58
+
59
+ def __js_set__(_key, _value)
60
+ nil
61
+ end
62
+ end
63
+
64
+ # `navigator.clipboard` — an in-memory clipboard for tests. Real
65
+ # OS clipboard access is intentionally not implemented; reads and
66
+ # writes round-trip through Ruby memory only.
67
+ #
68
+ # Async APIs (`readText`/`writeText`/`read`/`write`) return
69
+ # PromiseValue so callers' `.await` chains keep working.
70
+ class Clipboard
71
+ include EventTarget
72
+
73
+ def initialize(window)
74
+ @window = window
75
+ @text = ""
76
+ @items = []
77
+ end
78
+
79
+ # Sync read for tests that don't want to await.
80
+ def text
81
+ @text
82
+ end
83
+
84
+ def text=(value)
85
+ @text = value.to_s
86
+ end
87
+
88
+ def read_text
89
+ PromiseValue.resolve(@window, @text)
90
+ end
91
+
92
+ def write_text(text)
93
+ @text = text.to_s
94
+ PromiseValue.resolve(@window, nil)
95
+ end
96
+
97
+ def read
98
+ PromiseValue.resolve(@window, @items.dup)
99
+ end
100
+
101
+ def write(items)
102
+ @items = items.is_a?(Array) ? items : [items]
103
+ PromiseValue.resolve(@window, nil)
104
+ end
105
+
106
+ def __js_get__(_key)
107
+ nil
108
+ end
109
+
110
+ def __js_set__(_key, _value)
111
+ nil
112
+ end
113
+
114
+ def __js_call__(method, args)
115
+ case method
116
+ when "readText"
117
+ read_text
118
+ when "writeText"
119
+ write_text(args[0])
120
+ when "read"
121
+ read
122
+ when "write"
123
+ write(args[0])
124
+ when "addEventListener"
125
+ add_event_listener(args[0], args[1], args[2])
126
+ when "removeEventListener"
127
+ remove_event_listener(args[0], args[1])
128
+ when "dispatchEvent"
129
+ dispatch_event(args[0])
130
+ end
131
+ end
132
+
133
+ def __event_parent__
134
+ nil
135
+ end
136
+ end
137
+
138
+ # `navigator.permissions` — query returns a PermissionStatus whose
139
+ # `state` defaults to "granted" for every recognized name. Tests
140
+ # can override via `permissions.set("name", "denied")` before
141
+ # exercising user code.
142
+ class Permissions
143
+ KNOWN_NAMES = %w[
144
+ geolocation
145
+ notifications
146
+ push
147
+ midi
148
+ camera
149
+ microphone
150
+ clipboard-read
151
+ clipboard-write
152
+ background-fetch
153
+ background-sync
154
+ persistent-storage
155
+ accelerometer
156
+ gyroscope
157
+ magnetometer
158
+ screen-wake-lock
159
+ storage-access
160
+ window-management
161
+ ]
162
+ .freeze
163
+
164
+ def initialize(window)
165
+ @window = window
166
+ @overrides = {}
167
+ end
168
+
169
+ # Test helper: override the resolved state for a permission name.
170
+ # Subsequent `query()` calls will see the new value, and existing
171
+ # PermissionStatus objects fire `change` events.
172
+ def set(name, state)
173
+ key = name.to_s
174
+ @overrides[key] = state.to_s
175
+ @statuses ||= {}
176
+ status = @statuses[key]
177
+ status&.__set_state__(state.to_s)
178
+ nil
179
+ end
180
+
181
+ def query(descriptor)
182
+ name = if descriptor.is_a?(Hash)
183
+ (descriptor["name"] || descriptor[:name]).to_s
184
+ else
185
+ descriptor.to_s
186
+ end
187
+
188
+ state = @overrides[name] || "granted"
189
+ @statuses ||= {}
190
+ status = @statuses[name] ||= PermissionStatus.new(@window, name, state)
191
+ PromiseValue.resolve(@window, status)
192
+ end
193
+
194
+ def __js_get__(_key)
195
+ nil
196
+ end
197
+
198
+ def __js_set__(_key, _value)
199
+ nil
200
+ end
201
+
202
+ def __js_call__(method, args)
203
+ case method
204
+ when "query"
205
+ query(args[0])
206
+ end
207
+ end
208
+ end
209
+
210
+ # `PermissionStatus` — `state` + `onchange` event handler. Fires a
211
+ # `change` event when `Permissions#set` mutates the underlying
212
+ # value (mirrors browser behavior where the user toggles a
213
+ # permission).
214
+ class PermissionStatus
215
+ include EventTarget
216
+
217
+ attr_reader :name, :state
218
+
219
+ def initialize(window, name, state)
220
+ @window = window
221
+ @name = name
222
+ @state = state
223
+ @onchange = nil
224
+ end
225
+
226
+ def __set_state__(new_state)
227
+ return if @state == new_state
228
+
229
+ @state = new_state
230
+ dispatch_event(Event.new("change"))
231
+ end
232
+
233
+ def __js_get__(key)
234
+ case key
235
+ when "name"
236
+ @name
237
+ when "state"
238
+ @state
239
+ when "onchange"
240
+ @onchange
241
+ end
242
+ end
243
+
244
+ def __js_set__(key, value)
245
+ case key
246
+ when "onchange"
247
+ # Assigning to onchange overwrites the previous handler.
248
+ remove_event_listener("change", @onchange) if @onchange
249
+ @onchange = value
250
+ add_event_listener("change", value) if value
251
+ end
252
+
253
+ nil
254
+ end
255
+
256
+ def __js_call__(method, args)
257
+ case method
258
+ when "addEventListener"
259
+ add_event_listener(args[0], args[1], args[2])
260
+ when "removeEventListener"
261
+ remove_event_listener(args[0], args[1])
262
+ when "dispatchEvent"
263
+ dispatch_event(args[0])
264
+ end
265
+ end
266
+
267
+ def __event_parent__
268
+ nil
269
+ end
270
+ end
271
+ end
data/lib/dommy/node.rb ADDED
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `NodeList` — Array sub-class that adds the DOM NodeList surface
5
+ # (`item(i)` / `forEach(cb)` / `entries` / `keys` / `values`) on
6
+ # top of regular Array operations. Returned from
7
+ # `querySelectorAll`, `getElementsBy*`, `childNodes`, etc.
8
+ #
9
+ # Live vs. static collections aren't distinguished here — Dommy
10
+ # snapshots tree state at the time of the query, matching what
11
+ # most happy-dom test patterns expect.
12
+ class NodeList < Array
13
+ # Spec-compliant: out-of-range returns nil, not raise (Array#[] is
14
+ # close but we make negative indices fail too — DOM `item(-1)` is
15
+ # nil, not Array#[-1]'s last element).
16
+ def item(index)
17
+ i = index.to_i
18
+ return nil if i < 0 || i >= length
19
+
20
+ self[i]
21
+ end
22
+
23
+ # Spec signature: `forEach(callback(value, key, listObj))`. The
24
+ # Ruby `each_with_index` block-arg order is (value, index), which
25
+ # we re-yield as (value, index, self) for spec parity.
26
+ def for_each(&block)
27
+ each_with_index do |value, index|
28
+ block.call(value, index, self)
29
+ end
30
+
31
+ nil
32
+ end
33
+
34
+ alias forEach for_each
35
+
36
+ # NodeList `entries` returns an enumerator of [index, value].
37
+ def entries
38
+ each_with_index.map { |value, index| [index, value] }
39
+ end
40
+
41
+ def keys
42
+ (0...length).to_a
43
+ end
44
+
45
+ # `values` is the iterator of the NodeList itself; we return
46
+ # `self.to_a` (a plain Array copy) so callers can't mutate
47
+ # the original list.
48
+ def values
49
+ to_a
50
+ end
51
+
52
+ def __js_get__(key)
53
+ case key
54
+ when "length"
55
+ length
56
+ else
57
+ if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
58
+ item(key.to_i)
59
+ end
60
+ end
61
+ end
62
+
63
+ def __js_call__(method, args)
64
+ case method
65
+ when "item"
66
+ item(args[0])
67
+ when "forEach"
68
+ for_each(&args[0])
69
+ when "entries"
70
+ entries
71
+ when "keys"
72
+ keys
73
+ when "values"
74
+ values
75
+ end
76
+ end
77
+ end
78
+
79
+ # `LiveNodeList` — like NodeList, but re-evaluates its source on
80
+ # every access. Returned by APIs whose spec says "live" — e.g.
81
+ # `Node.childNodes`. The constructor takes a block that yields the
82
+ # current array of nodes; `length`, `item`, iteration all call it.
83
+ #
84
+ # Inherits Array so `list[i]` / `list.each` still work for callers
85
+ # that don't know about the live semantics, but those work off a
86
+ # snapshot taken at the moment of the call. The DOM-shape methods
87
+ # (`length`, `item`, `for_each`) re-query on every call.
88
+ class LiveNodeList
89
+ include Enumerable
90
+
91
+ def initialize(&block)
92
+ @compute = block
93
+ end
94
+
95
+ def length
96
+ @compute.call.length
97
+ end
98
+
99
+ alias size length
100
+
101
+ def item(index)
102
+ i = index.to_i
103
+ arr = @compute.call
104
+ return nil if i < 0 || i >= arr.length
105
+
106
+ arr[i]
107
+ end
108
+
109
+ def [](index)
110
+ case index
111
+ when Integer
112
+ item(index)
113
+ else
114
+ nil
115
+ end
116
+ end
117
+
118
+ def first
119
+ @compute.call.first
120
+ end
121
+
122
+ def last
123
+ @compute.call.last
124
+ end
125
+
126
+ def each(&block)
127
+ @compute.call.each(&block)
128
+ self
129
+ end
130
+
131
+ def to_a
132
+ @compute.call.dup
133
+ end
134
+
135
+ def for_each(&block)
136
+ @compute.call.each_with_index do |value, index|
137
+ block.call(value, index, self)
138
+ end
139
+
140
+ nil
141
+ end
142
+
143
+ alias forEach for_each
144
+
145
+ def entries
146
+ @compute.call.each_with_index.map { |v, i| [i, v] }
147
+ end
148
+
149
+ def keys
150
+ (0...length).to_a
151
+ end
152
+
153
+ def values
154
+ to_a
155
+ end
156
+
157
+ def empty?
158
+ @compute.call.empty?
159
+ end
160
+
161
+ def __js_get__(key)
162
+ case key
163
+ when "length"
164
+ length
165
+ else
166
+ if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
167
+ item(key.to_i)
168
+ end
169
+ end
170
+ end
171
+
172
+ def __js_call__(method, args)
173
+ case method
174
+ when "item"
175
+ item(args[0])
176
+ when "forEach"
177
+ for_each(&args[0])
178
+ when "entries"
179
+ entries
180
+ when "keys"
181
+ keys
182
+ when "values"
183
+ values
184
+ end
185
+ end
186
+ end
187
+
188
+ # `Node` — common base mixin. All node-like classes (Element,
189
+ # TextNode, CommentNode, CharacterDataNode, Document, Fragment,
190
+ # DocumentType, ShadowRoot) include this so `el.is_a?(Dommy::Node)`
191
+ # works.
192
+ #
193
+ # Real classes already define `nodeType` / `nodeName` / `nodeValue`
194
+ # / `parentNode` / `isConnected` / `cloneNode` independently; this
195
+ # module is primarily an identity marker. Adding new shared methods
196
+ # later is straightforward.
197
+ module Node
198
+ # Standardized nodeType constants — duplicated from Element so
199
+ # callers can refer to `Dommy::Node::ELEMENT_NODE` without
200
+ # depending on a specific subclass.
201
+ ELEMENT_NODE = 1
202
+ ATTRIBUTE_NODE = 2
203
+ TEXT_NODE = 3
204
+ CDATA_SECTION_NODE = 4
205
+ PROCESSING_INSTRUCTION_NODE = 7
206
+ COMMENT_NODE = 8
207
+ DOCUMENT_NODE = 9
208
+ DOCUMENT_TYPE_NODE = 10
209
+ DOCUMENT_FRAGMENT_NODE = 11
210
+
211
+ DOCUMENT_POSITION_DISCONNECTED = 0x01
212
+ DOCUMENT_POSITION_PRECEDING = 0x02
213
+ DOCUMENT_POSITION_FOLLOWING = 0x04
214
+ DOCUMENT_POSITION_CONTAINS = 0x08
215
+ DOCUMENT_POSITION_CONTAINED_BY = 0x10
216
+ DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20
217
+ end
218
+ end