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.
- checksums.yaml +7 -0
- data/README.md +213 -0
- data/lib/dommy/attr.rb +200 -0
- data/lib/dommy/blob.rb +182 -0
- data/lib/dommy/bridge.rb +141 -0
- data/lib/dommy/css.rb +283 -0
- data/lib/dommy/custom_elements.rb +125 -0
- data/lib/dommy/data_transfer.rb +98 -0
- data/lib/dommy/document.rb +674 -0
- data/lib/dommy/dom_exception.rb +258 -0
- data/lib/dommy/dom_parser.rb +88 -0
- data/lib/dommy/element.rb +1975 -0
- data/lib/dommy/event.rb +589 -0
- data/lib/dommy/fetch.rb +241 -0
- data/lib/dommy/form_data.rb +208 -0
- data/lib/dommy/html_collection.rb +207 -0
- data/lib/dommy/html_elements.rb +4455 -0
- data/lib/dommy/internal/cookie_jar.rb +27 -0
- data/lib/dommy/internal/dom_matching.rb +141 -0
- data/lib/dommy/internal/mutation_coordinator.rb +172 -0
- data/lib/dommy/internal/node_traversal.rb +36 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
- data/lib/dommy/internal/observer_manager.rb +31 -0
- data/lib/dommy/internal/observer_matcher.rb +31 -0
- data/lib/dommy/internal/scope_resolution.rb +27 -0
- data/lib/dommy/internal/shadow_root_registry.rb +35 -0
- data/lib/dommy/internal/template_content_registry.rb +97 -0
- data/lib/dommy/minitest/assertions.rb +105 -0
- data/lib/dommy/minitest.rb +17 -0
- data/lib/dommy/navigator.rb +271 -0
- data/lib/dommy/node.rb +218 -0
- data/lib/dommy/observer.rb +199 -0
- data/lib/dommy/parser.rb +29 -0
- data/lib/dommy/promise.rb +199 -0
- data/lib/dommy/router.rb +275 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
- data/lib/dommy/rspec/matchers.rb +230 -0
- data/lib/dommy/rspec.rb +18 -0
- data/lib/dommy/scheduler.rb +135 -0
- data/lib/dommy/shadow_root.rb +255 -0
- data/lib/dommy/storage.rb +112 -0
- data/lib/dommy/test_helpers.rb +78 -0
- data/lib/dommy/tree_walker.rb +425 -0
- data/lib/dommy/url.rb +479 -0
- data/lib/dommy/version.rb +5 -0
- data/lib/dommy/world.rb +209 -0
- data/lib/dommy.rb +119 -0
- metadata +110 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# Deterministic host-side scheduler for timers, rAF, and microtasks.
|
|
5
|
+
# Time advances only when the host explicitly calls `advance_time`.
|
|
6
|
+
class Scheduler
|
|
7
|
+
Timer = Struct.new(:id, :kind, :callback, :due_at, :interval_ms, :active)
|
|
8
|
+
|
|
9
|
+
FRAME_MS = 16
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@now_ms = 0
|
|
13
|
+
@next_id = 1
|
|
14
|
+
@timers = {}
|
|
15
|
+
@microtasks = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :now_ms
|
|
19
|
+
|
|
20
|
+
def set_timeout(callback, delay_ms)
|
|
21
|
+
register_timer(:timeout, callback, delay_ms.to_i, nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear_timeout(id)
|
|
25
|
+
cancel_timer(id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set_interval(callback, interval_ms)
|
|
29
|
+
ms = [interval_ms.to_i, 0].max
|
|
30
|
+
register_timer(:interval, callback, ms, ms)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear_interval(id)
|
|
34
|
+
cancel_timer(id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def request_animation_frame(callback)
|
|
38
|
+
frames = ((@now_ms / FRAME_MS) + 1) * FRAME_MS
|
|
39
|
+
id = next_id
|
|
40
|
+
@timers[id] = Timer.new(id, :raf, callback, frames, nil, true)
|
|
41
|
+
id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cancel_animation_frame(id)
|
|
45
|
+
cancel_timer(id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def queue_microtask(callback)
|
|
49
|
+
@microtasks << callback
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def drain_microtasks
|
|
54
|
+
until @microtasks.empty?
|
|
55
|
+
callback = @microtasks.shift
|
|
56
|
+
invoke_callback(callback, [@now_ms])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def advance_time(delta_ms)
|
|
63
|
+
target = @now_ms + [delta_ms.to_i, 0].max
|
|
64
|
+
while next_due_timer_at && next_due_timer_at <= target
|
|
65
|
+
@now_ms = next_due_timer_at
|
|
66
|
+
run_due_timers
|
|
67
|
+
drain_microtasks
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@now_ms = target
|
|
71
|
+
drain_microtasks
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def drain_timers(advance: 0)
|
|
76
|
+
advance_time(advance)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Public accessor for eval-time auto-drain: keep advancing the
|
|
80
|
+
# clock until no timers remain (or a safety budget runs out).
|
|
81
|
+
def next_due_timer_at
|
|
82
|
+
@timers.values.select(&:active).map(&:due_at).min
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def next_id
|
|
88
|
+
id = @next_id
|
|
89
|
+
@next_id += 1
|
|
90
|
+
id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def register_timer(kind, callback, delay_ms, interval_ms)
|
|
94
|
+
id = next_id
|
|
95
|
+
due_at = @now_ms + [delay_ms, 0].max
|
|
96
|
+
@timers[id] = Timer.new(id, kind, callback, due_at, interval_ms, true)
|
|
97
|
+
id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cancel_timer(id)
|
|
101
|
+
timer = @timers[id.to_i]
|
|
102
|
+
timer.active = false if timer
|
|
103
|
+
@timers.delete(id.to_i)
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def run_due_timers
|
|
108
|
+
due = @timers.values.select { |timer| timer.active && timer.due_at <= @now_ms }
|
|
109
|
+
due.sort_by!(&:id)
|
|
110
|
+
due.each do |timer|
|
|
111
|
+
next unless timer.active
|
|
112
|
+
|
|
113
|
+
case timer.kind
|
|
114
|
+
when :raf
|
|
115
|
+
@timers.delete(timer.id)
|
|
116
|
+
invoke_callback(timer.callback, [@now_ms.to_f])
|
|
117
|
+
when :interval
|
|
118
|
+
invoke_callback(timer.callback, [])
|
|
119
|
+
timer.due_at = @now_ms + timer.interval_ms if timer.active
|
|
120
|
+
else
|
|
121
|
+
@timers.delete(timer.id)
|
|
122
|
+
invoke_callback(timer.callback, [])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def invoke_callback(callback, args)
|
|
128
|
+
if callback.respond_to?(:__js_call__)
|
|
129
|
+
callback.__js_call__("call", args)
|
|
130
|
+
elsif callback.respond_to?(:call)
|
|
131
|
+
callback.call(*args)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `ShadowRoot` — a DocumentFragment-shaped subtree attached to a
|
|
5
|
+
# host Element via `attachShadow`. Lives in its own Nokogiri
|
|
6
|
+
# fragment that's invisible to the outer document's tree walks
|
|
7
|
+
# (querySelector, getElementById, children, etc.), which is the
|
|
8
|
+
# core "encapsulation" the spec promises.
|
|
9
|
+
#
|
|
10
|
+
# Tree manipulation works the same as a normal Element/Fragment;
|
|
11
|
+
# the boundary is enforced only on outer queries and event
|
|
12
|
+
# composition. CSS scoping (`:host`, `::slotted`) is out of scope.
|
|
13
|
+
class ShadowRoot
|
|
14
|
+
include EventTarget
|
|
15
|
+
include Node
|
|
16
|
+
|
|
17
|
+
attr_reader :__node__, :host, :mode, :delegates_focus, :slot_assignment, :document
|
|
18
|
+
|
|
19
|
+
def initialize(host, mode:, delegates_focus: false, slot_assignment: "named")
|
|
20
|
+
@host = host
|
|
21
|
+
@mode = mode.to_s
|
|
22
|
+
@delegates_focus = !!delegates_focus
|
|
23
|
+
@slot_assignment = slot_assignment.to_s
|
|
24
|
+
@document = host.document
|
|
25
|
+
@__node__ = @document.nokogiri_doc.fragment("")
|
|
26
|
+
@document.__register_shadow_fragment__(@__node__, self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# ---- Public Ruby API (ParentNode + DocumentFragment mixin) ----
|
|
30
|
+
|
|
31
|
+
def inner_html
|
|
32
|
+
@__node__.children.map(&:to_html).join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inner_html=(html)
|
|
36
|
+
removed = @__node__.children.to_a
|
|
37
|
+
removed.each(&:unlink)
|
|
38
|
+
fragment = Parser.fragment(html.to_s, owner_doc: @document.nokogiri_doc)
|
|
39
|
+
added = fragment.children.to_a
|
|
40
|
+
added.each { |n| @__node__.add_child(n) }
|
|
41
|
+
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: added, removed_nodes: removed)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def text_content
|
|
46
|
+
@__node__.text
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def text_content=(value)
|
|
50
|
+
@__node__.children.each(&:unlink)
|
|
51
|
+
@__node__.add_child(Nokogiri::XML::Text.new(value.to_s, @document.nokogiri_doc))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def children
|
|
55
|
+
@__node__.element_children.map { |n| @document.wrap_node(n) }.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def child_nodes
|
|
59
|
+
@__node__.children.map { |n| @document.wrap_node(n) }.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def child_element_count
|
|
63
|
+
@__node__.element_children.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def first_child
|
|
67
|
+
@document.wrap_node(@__node__.children.first)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def last_child
|
|
71
|
+
@document.wrap_node(@__node__.children.last)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def first_element_child
|
|
75
|
+
@document.wrap_node(@__node__.element_children.first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def last_element_child
|
|
79
|
+
@document.wrap_node(@__node__.element_children.last)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def append_child(child)
|
|
83
|
+
nodes = detach_dom_nodes(child)
|
|
84
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
85
|
+
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
86
|
+
child
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def append(*args)
|
|
90
|
+
nodes = args.flat_map { |a| detach_dom_nodes(a) }
|
|
91
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
92
|
+
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prepend(*args)
|
|
97
|
+
nodes = args.flat_map { |a| detach_dom_nodes(a) }
|
|
98
|
+
anchor = @__node__.children.first
|
|
99
|
+
if anchor
|
|
100
|
+
nodes.reverse_each { |n| anchor.add_previous_sibling(n) }
|
|
101
|
+
else
|
|
102
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def replace_children(*args)
|
|
110
|
+
removed = @__node__.children.to_a
|
|
111
|
+
removed.each(&:unlink)
|
|
112
|
+
nodes = args.flat_map { |a| detach_dom_nodes(a) }
|
|
113
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
114
|
+
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: removed)
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def query_selector(selector)
|
|
119
|
+
return nil if selector.nil? || selector.to_s.empty?
|
|
120
|
+
|
|
121
|
+
@document.wrap_node(@__node__.at_css(selector.to_s))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def query_selector_all(selector)
|
|
125
|
+
return NodeList.new if selector.nil? || selector.to_s.empty?
|
|
126
|
+
|
|
127
|
+
NodeList.new(@__node__.css(selector.to_s).map { |n| @document.wrap_node(n) }.compact)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def get_element_by_id(id)
|
|
131
|
+
return nil if id.nil?
|
|
132
|
+
|
|
133
|
+
@document.wrap_node(@__node__.at_css("##{id}"))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `getRootNode()` returns the ShadowRoot itself (closed-shadow
|
|
137
|
+
# semantics; `composed: true` callers go through the Event path).
|
|
138
|
+
def get_root_node(_options = nil)
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def contains?(other)
|
|
143
|
+
return false unless other.respond_to?(:__node__)
|
|
144
|
+
|
|
145
|
+
other_node = other.__node__
|
|
146
|
+
return true if other_node == @__node__
|
|
147
|
+
|
|
148
|
+
Internal::NodeTraversal.ancestor_of?(@__node__, other_node)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `[]` accessor mirrors the bracket convention used elsewhere.
|
|
152
|
+
def [](key)
|
|
153
|
+
__js_get__(key.to_s)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def []=(k, v)
|
|
157
|
+
__js_set__(k.to_s, v)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def __js_get__(key)
|
|
161
|
+
case key
|
|
162
|
+
when "host"
|
|
163
|
+
@host
|
|
164
|
+
when "mode"
|
|
165
|
+
@mode
|
|
166
|
+
when "delegatesFocus"
|
|
167
|
+
@delegates_focus
|
|
168
|
+
when "slotAssignment"
|
|
169
|
+
@slot_assignment
|
|
170
|
+
when "innerHTML"
|
|
171
|
+
inner_html
|
|
172
|
+
when "textContent"
|
|
173
|
+
text_content
|
|
174
|
+
when "children"
|
|
175
|
+
children
|
|
176
|
+
when "childNodes"
|
|
177
|
+
child_nodes
|
|
178
|
+
when "childElementCount"
|
|
179
|
+
child_element_count
|
|
180
|
+
when "firstChild"
|
|
181
|
+
first_child
|
|
182
|
+
when "lastChild"
|
|
183
|
+
last_child
|
|
184
|
+
when "firstElementChild"
|
|
185
|
+
first_element_child
|
|
186
|
+
when "lastElementChild"
|
|
187
|
+
last_element_child
|
|
188
|
+
when "nodeType"
|
|
189
|
+
11
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def __js_set__(key, value)
|
|
194
|
+
case key
|
|
195
|
+
when "innerHTML"
|
|
196
|
+
self.inner_html = value
|
|
197
|
+
when "textContent"
|
|
198
|
+
self.text_content = value
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def __js_call__(method, args)
|
|
205
|
+
case method
|
|
206
|
+
when "querySelector"
|
|
207
|
+
query_selector(args[0])
|
|
208
|
+
when "querySelectorAll"
|
|
209
|
+
query_selector_all(args[0])
|
|
210
|
+
when "getElementById"
|
|
211
|
+
get_element_by_id(args[0])
|
|
212
|
+
when "append"
|
|
213
|
+
append(*args)
|
|
214
|
+
when "prepend"
|
|
215
|
+
prepend(*args)
|
|
216
|
+
when "replaceChildren"
|
|
217
|
+
replace_children(*args)
|
|
218
|
+
when "appendChild"
|
|
219
|
+
append_child(args[0])
|
|
220
|
+
when "getRootNode"
|
|
221
|
+
get_root_node(args[0])
|
|
222
|
+
when "contains"
|
|
223
|
+
contains?(args[0])
|
|
224
|
+
when "addEventListener"
|
|
225
|
+
add_event_listener(args[0], args[1], args[2])
|
|
226
|
+
when "removeEventListener"
|
|
227
|
+
remove_event_listener(args[0], args[1])
|
|
228
|
+
when "dispatchEvent"
|
|
229
|
+
dispatch_event(args[0])
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Event bubbling stops at the ShadowRoot unless event has
|
|
234
|
+
# `composed: true`. The host is the bubble-path successor when
|
|
235
|
+
# composition crosses the boundary (handled in Event dispatch).
|
|
236
|
+
def __event_parent__
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def detach_dom_nodes(value)
|
|
243
|
+
case value
|
|
244
|
+
when String
|
|
245
|
+
[Nokogiri::XML::Text.new(value, @document.nokogiri_doc)]
|
|
246
|
+
else
|
|
247
|
+
node = value.respond_to?(:__node__) ? value.__node__ : nil
|
|
248
|
+
return [] unless node
|
|
249
|
+
|
|
250
|
+
node.unlink if node.parent
|
|
251
|
+
[node]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# Hash-backed `Storage` polyfill for `localStorage` /
|
|
5
|
+
# `sessionStorage`. Mirrors the Web Storage API:
|
|
6
|
+
# `getItem(key)`, `setItem(key, value)`, `removeItem(key)`,
|
|
7
|
+
# `clear()`, `key(index)`, `length`. Values are coerced to String
|
|
8
|
+
# to match browser semantics (browser stores everything as String).
|
|
9
|
+
#
|
|
10
|
+
# No persistence across Window instances — each fresh Window gets
|
|
11
|
+
# an empty Storage. Tests that depend on cross-instance behaviour
|
|
12
|
+
# (none currently) would need explicit hydration.
|
|
13
|
+
class Storage
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@store = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Ruby-idiomatic facade matching `Object.keys(storage)` /
|
|
21
|
+
# `Object.values(storage)` / `Object.entries(storage)` semantics
|
|
22
|
+
# that user code reaches for in browser JS.
|
|
23
|
+
|
|
24
|
+
def keys
|
|
25
|
+
@store.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def values
|
|
29
|
+
@store.values
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def entries
|
|
33
|
+
@store.to_a
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
@store.dup
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each(&blk)
|
|
41
|
+
@store.each(&blk)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def length
|
|
45
|
+
@store.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias size length
|
|
49
|
+
|
|
50
|
+
def get_item(key)
|
|
51
|
+
@store[key.to_s]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set_item(key, value)
|
|
55
|
+
@store[key.to_s] = value.to_s
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def remove_item(key)
|
|
60
|
+
@store.delete(key.to_s)
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear
|
|
65
|
+
@store.clear
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def key(index)
|
|
70
|
+
@store.keys[index.to_i]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def [](key)
|
|
74
|
+
@store[key.to_s]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def []=(key, value)
|
|
78
|
+
@store[key.to_s] = value.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def __js_get__(key)
|
|
82
|
+
case key
|
|
83
|
+
when "length"
|
|
84
|
+
@store.size
|
|
85
|
+
else
|
|
86
|
+
@store[key.to_s]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def __js_set__(key, value)
|
|
91
|
+
@store[key.to_s] = value.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def __js_call__(method, args)
|
|
95
|
+
case method
|
|
96
|
+
when "getItem"
|
|
97
|
+
@store[args[0].to_s]
|
|
98
|
+
when "setItem"
|
|
99
|
+
@store[args[0].to_s] = args[1].to_s
|
|
100
|
+
nil
|
|
101
|
+
when "removeItem"
|
|
102
|
+
@store.delete(args[0].to_s)
|
|
103
|
+
nil
|
|
104
|
+
when "clear"
|
|
105
|
+
@store.clear
|
|
106
|
+
nil
|
|
107
|
+
when "key"
|
|
108
|
+
@store.keys[args[0].to_i]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# Lightweight helpers for using Dommy from RSpec / Minitest test suites.
|
|
5
|
+
#
|
|
6
|
+
# @example RSpec
|
|
7
|
+
# require "dommy/test_helpers"
|
|
8
|
+
#
|
|
9
|
+
# RSpec.configure do |c|
|
|
10
|
+
# c.include Dommy::TestHelpers
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# RSpec.describe MyComponent do
|
|
14
|
+
# it "renders the heading" do
|
|
15
|
+
# dom = parse_html(render(MyComponent.new))
|
|
16
|
+
# expect(dom.query_selector("h1").text_content).to eq("Welcome")
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Minitest
|
|
21
|
+
# require "dommy/test_helpers"
|
|
22
|
+
#
|
|
23
|
+
# class MyComponentTest < Minitest::Test
|
|
24
|
+
# include Dommy::TestHelpers
|
|
25
|
+
#
|
|
26
|
+
# def test_renders_the_heading
|
|
27
|
+
# dom = parse_html(render(MyComponent.new))
|
|
28
|
+
# assert_equal "Welcome", dom.query_selector("h1").text_content
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
module TestHelpers
|
|
32
|
+
# Parse an HTML string into a fresh Document and return it.
|
|
33
|
+
#
|
|
34
|
+
# When the input starts with `<!doctype` or `<html>`, it is parsed as
|
|
35
|
+
# a full HTML document (preserving <head>, <title>, etc.). Otherwise
|
|
36
|
+
# the input is treated as a body fragment and inserted into a fresh
|
|
37
|
+
# document's <body>.
|
|
38
|
+
#
|
|
39
|
+
# @param html [String] HTML to parse (full document or body fragment)
|
|
40
|
+
# @return [Dommy::Document] a fresh Document with the parsed content
|
|
41
|
+
def parse_html(html = "")
|
|
42
|
+
Dommy.parse(html).document
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build a fresh Window with the given body HTML.
|
|
46
|
+
# When a block is given, yields the window first; the same window
|
|
47
|
+
# is returned in both cases so callers can choose their style.
|
|
48
|
+
#
|
|
49
|
+
# @param body_html [String] HTML to insert inside <body>
|
|
50
|
+
# @yieldparam window [Dommy::Window]
|
|
51
|
+
# @return [Dommy::Window] the new Window
|
|
52
|
+
def make_window(body_html = "")
|
|
53
|
+
window = Dommy::Window.new
|
|
54
|
+
window.document.body.inner_html = body_html.to_s
|
|
55
|
+
yield window if block_given?
|
|
56
|
+
window
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Drain pending microtasks on the window's scheduler.
|
|
60
|
+
# Use this after a mutation if you need MutationObserver callbacks
|
|
61
|
+
# (scheduled as microtasks) to fire before your assertions.
|
|
62
|
+
#
|
|
63
|
+
# @param window [Dommy::Window]
|
|
64
|
+
def flush_microtasks(window)
|
|
65
|
+
window.scheduler.drain_microtasks
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Advance the window's virtual clock. Timers that come due and
|
|
69
|
+
# any queued microtasks are run as part of the advance.
|
|
70
|
+
# Use this to test code that schedules work with setTimeout / setInterval.
|
|
71
|
+
#
|
|
72
|
+
# @param window [Dommy::Window]
|
|
73
|
+
# @param ms [Integer] milliseconds to advance
|
|
74
|
+
def advance_time(window, ms)
|
|
75
|
+
window.scheduler.advance_time(ms)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|