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.
- checksums.yaml +4 -4
- data/README.md +30 -38
- data/lib/dommy/animation.rb +1 -1
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +1 -1
- data/lib/dommy/crypto.rb +9 -8
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +98 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +231 -50
- data/lib/dommy/event.rb +61 -25
- data/lib/dommy/event_source.rb +8 -8
- data/lib/dommy/fetch.rb +14 -6
- data/lib/dommy/file_reader.rb +3 -3
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +7 -4
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +110 -42
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +2 -2
- data/lib/dommy/location.rb +8 -4
- data/lib/dommy/media_query_list.rb +3 -3
- data/lib/dommy/message_channel.rb +9 -9
- data/lib/dommy/mutation_observer.rb +21 -11
- data/lib/dommy/navigator.rb +12 -12
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +3 -3
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance_observer.rb +2 -2
- data/lib/dommy/range.rb +2 -2
- data/lib/dommy/resize_observer.rb +2 -2
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +22 -22
- data/lib/dommy/text_codec.rb +4 -4
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +25 -8
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +13 -13
- data/lib/dommy/window.rb +14 -1
- data/lib/dommy/worker.rb +5 -5
- data/lib/dommy/xml_http_request.rb +19 -4
- data/lib/dommy.rb +12 -2
- metadata +12 -26
data/lib/dommy/event.rb
CHANGED
|
@@ -54,24 +54,30 @@ module Dommy
|
|
|
54
54
|
# Per spec, dispatchEvent must receive an Event instance.
|
|
55
55
|
raise TypeError, "dispatchEvent requires an Event, got #{event.class}" unless event.is_a?(Event)
|
|
56
56
|
|
|
57
|
-
event.
|
|
57
|
+
event.__internal_prepare_for_dispatch__(self)
|
|
58
58
|
path = if event.bubbles?
|
|
59
59
|
event.__js_get__("composed") ? composed_bubble_path(event) : event_bubble_path
|
|
60
60
|
else
|
|
61
61
|
[self]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
event.
|
|
64
|
+
event.__internal_record_path__(path) if event.respond_to?(:__internal_record_path__)
|
|
65
65
|
path.each do |target|
|
|
66
|
-
event.
|
|
67
|
-
target.
|
|
66
|
+
event.__internal_set_current_target__(target)
|
|
67
|
+
target.__internal_deliver_event__(event)
|
|
68
68
|
break if event.propagation_stopped?
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# WHATWG: after dispatch completes, currentTarget reverts to
|
|
72
|
+
# null and eventPhase reverts to NONE. Dommy derives
|
|
73
|
+
# eventPhase from currentTarget, so clearing it here covers
|
|
74
|
+
# both spec requirements.
|
|
75
|
+
event.__internal_set_current_target__(nil)
|
|
76
|
+
|
|
71
77
|
!event.default_prevented?
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
def
|
|
80
|
+
def __internal_deliver_event__(event)
|
|
75
81
|
listeners = listeners_for(event.type).dup
|
|
76
82
|
listeners.each do |entry|
|
|
77
83
|
invoke_listener(entry.listener, event)
|
|
@@ -106,7 +112,7 @@ module Dommy
|
|
|
106
112
|
def event_bubble_path
|
|
107
113
|
path = [self]
|
|
108
114
|
current = self
|
|
109
|
-
while (current = current.send(:
|
|
115
|
+
while (current = current.send(:__internal_event_parent__))
|
|
110
116
|
path << current
|
|
111
117
|
end
|
|
112
118
|
|
|
@@ -116,12 +122,12 @@ module Dommy
|
|
|
116
122
|
# Build the propagation path with optional shadow-boundary
|
|
117
123
|
# crossing. When the in-flight event has `composed: true`, the
|
|
118
124
|
# walk continues from a ShadowRoot to its host; otherwise it
|
|
119
|
-
# stops at the shadow boundary (nil from `
|
|
125
|
+
# stops at the shadow boundary (nil from `__internal_event_parent__`).
|
|
120
126
|
def composed_bubble_path(event)
|
|
121
127
|
path = [self]
|
|
122
128
|
current = self
|
|
123
129
|
loop do
|
|
124
|
-
nxt = current.send(:
|
|
130
|
+
nxt = current.send(:__internal_event_parent__)
|
|
125
131
|
if nxt.nil? && event.respond_to?(:__js_get__) && event.__js_get__("composed")
|
|
126
132
|
# Try to cross a shadow boundary
|
|
127
133
|
if current.is_a?(ShadowRoot)
|
|
@@ -148,12 +154,12 @@ module Dommy
|
|
|
148
154
|
private
|
|
149
155
|
|
|
150
156
|
def enclosing_shadow_root_of(target)
|
|
151
|
-
return nil unless target.respond_to?(:
|
|
157
|
+
return nil unless target.respond_to?(:__dommy_backend_node__)
|
|
152
158
|
|
|
153
159
|
doc = target.instance_variable_get(:@document)
|
|
154
|
-
return nil unless doc && doc.respond_to?(:
|
|
160
|
+
return nil unless doc && doc.respond_to?(:__internal_shadow_root_containing__)
|
|
155
161
|
|
|
156
|
-
doc.
|
|
162
|
+
doc.__internal_shadow_root_containing__(target.__dommy_backend_node__)
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
public
|
|
@@ -178,6 +184,12 @@ module Dommy
|
|
|
178
184
|
class StandaloneEventTarget
|
|
179
185
|
include EventTarget
|
|
180
186
|
|
|
187
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
188
|
+
JS_METHOD_NAMES = %w[addEventListener removeEventListener dispatchEvent].freeze
|
|
189
|
+
def __js_method_names__
|
|
190
|
+
JS_METHOD_NAMES
|
|
191
|
+
end
|
|
192
|
+
|
|
181
193
|
def __js_call__(method, args)
|
|
182
194
|
case method
|
|
183
195
|
when "addEventListener"
|
|
@@ -191,7 +203,7 @@ module Dommy
|
|
|
191
203
|
end
|
|
192
204
|
end
|
|
193
205
|
|
|
194
|
-
def
|
|
206
|
+
def __internal_event_parent__
|
|
195
207
|
nil
|
|
196
208
|
end
|
|
197
209
|
end
|
|
@@ -232,11 +244,11 @@ module Dommy
|
|
|
232
244
|
@immediate_propagation_stopped
|
|
233
245
|
end
|
|
234
246
|
|
|
235
|
-
def
|
|
247
|
+
def __internal_prepare_for_dispatch__(target)
|
|
236
248
|
@target ||= target
|
|
237
249
|
end
|
|
238
250
|
|
|
239
|
-
def
|
|
251
|
+
def __internal_set_current_target__(target)
|
|
240
252
|
@current_target = target
|
|
241
253
|
end
|
|
242
254
|
|
|
@@ -276,6 +288,12 @@ module Dommy
|
|
|
276
288
|
nil
|
|
277
289
|
end
|
|
278
290
|
|
|
291
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
292
|
+
JS_METHOD_NAMES = %w[preventDefault stopPropagation stopImmediatePropagation composedPath initEvent].freeze
|
|
293
|
+
def __js_method_names__
|
|
294
|
+
JS_METHOD_NAMES
|
|
295
|
+
end
|
|
296
|
+
|
|
279
297
|
def __js_call__(method, args)
|
|
280
298
|
case method
|
|
281
299
|
when "preventDefault"
|
|
@@ -314,7 +332,7 @@ module Dommy
|
|
|
314
332
|
# Per spec, `load` events do not propagate to the Window when
|
|
315
333
|
# composed paths are computed (resource-finished signal stays at
|
|
316
334
|
# the target).
|
|
317
|
-
def
|
|
335
|
+
def __internal_record_path__(targets)
|
|
318
336
|
@composed_path = if @type == "load"
|
|
319
337
|
targets.reject { |t| t.is_a?(Window) }
|
|
320
338
|
else
|
|
@@ -640,6 +658,12 @@ module Dommy
|
|
|
640
658
|
end
|
|
641
659
|
end
|
|
642
660
|
|
|
661
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
662
|
+
JS_METHOD_NAMES = %w[item].freeze
|
|
663
|
+
def __js_method_names__
|
|
664
|
+
JS_METHOD_NAMES
|
|
665
|
+
end
|
|
666
|
+
|
|
643
667
|
def __js_call__(method, args)
|
|
644
668
|
case method
|
|
645
669
|
when "item"
|
|
@@ -823,7 +847,7 @@ module Dommy
|
|
|
823
847
|
# signal. Convenient for APIs that need an already-cancelled token.
|
|
824
848
|
def self.abort(reason = nil)
|
|
825
849
|
signal = new
|
|
826
|
-
signal.
|
|
850
|
+
signal.__internal_mark_aborted__(reason)
|
|
827
851
|
signal
|
|
828
852
|
end
|
|
829
853
|
|
|
@@ -836,9 +860,9 @@ module Dommy
|
|
|
836
860
|
signal = new
|
|
837
861
|
reason = DOMException::TimeoutError.new("operation timed out")
|
|
838
862
|
if scheduler
|
|
839
|
-
scheduler.set_timeout(proc { signal.
|
|
863
|
+
scheduler.set_timeout(proc { signal.__internal_mark_aborted__(reason) }, ms.to_i)
|
|
840
864
|
else
|
|
841
|
-
signal.
|
|
865
|
+
signal.__internal_schedule_thread_timeout__(ms.to_i, reason)
|
|
842
866
|
end
|
|
843
867
|
|
|
844
868
|
signal
|
|
@@ -853,12 +877,12 @@ module Dommy
|
|
|
853
877
|
list = Array(signals).select { |s| s.is_a?(AbortSignal) }
|
|
854
878
|
already = list.find(&:aborted?)
|
|
855
879
|
if already
|
|
856
|
-
composite.
|
|
880
|
+
composite.__internal_mark_aborted__(already.reason)
|
|
857
881
|
return composite
|
|
858
882
|
end
|
|
859
883
|
|
|
860
884
|
list.each do |sig|
|
|
861
|
-
sig.add_event_listener("abort", proc { composite.
|
|
885
|
+
sig.add_event_listener("abort", proc { composite.__internal_mark_aborted__(sig.reason) })
|
|
862
886
|
end
|
|
863
887
|
|
|
864
888
|
composite
|
|
@@ -871,11 +895,11 @@ module Dommy
|
|
|
871
895
|
|
|
872
896
|
# Background-thread timeout used by `AbortSignal.timeout` when no
|
|
873
897
|
# scheduler is provided. Kept package-private; tests can also
|
|
874
|
-
# drive the abort manually via `
|
|
875
|
-
def
|
|
898
|
+
# drive the abort manually via `__internal_mark_aborted__`.
|
|
899
|
+
def __internal_schedule_thread_timeout__(ms, reason)
|
|
876
900
|
Thread.new do
|
|
877
901
|
sleep(ms.to_f / 1000.0)
|
|
878
|
-
|
|
902
|
+
__internal_mark_aborted__(reason)
|
|
879
903
|
end
|
|
880
904
|
|
|
881
905
|
nil
|
|
@@ -912,6 +936,12 @@ module Dommy
|
|
|
912
936
|
nil
|
|
913
937
|
end
|
|
914
938
|
|
|
939
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
940
|
+
JS_METHOD_NAMES = %w[addEventListener removeEventListener dispatchEvent throwIfAborted].freeze
|
|
941
|
+
def __js_method_names__
|
|
942
|
+
JS_METHOD_NAMES
|
|
943
|
+
end
|
|
944
|
+
|
|
915
945
|
def __js_call__(method, args)
|
|
916
946
|
case method
|
|
917
947
|
when "addEventListener"
|
|
@@ -925,7 +955,7 @@ module Dommy
|
|
|
925
955
|
end
|
|
926
956
|
end
|
|
927
957
|
|
|
928
|
-
def
|
|
958
|
+
def __internal_mark_aborted__(reason = nil)
|
|
929
959
|
return if @aborted
|
|
930
960
|
|
|
931
961
|
@aborted = true
|
|
@@ -949,10 +979,16 @@ module Dommy
|
|
|
949
979
|
nil
|
|
950
980
|
end
|
|
951
981
|
|
|
982
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
983
|
+
JS_METHOD_NAMES = %w[abort].freeze
|
|
984
|
+
def __js_method_names__
|
|
985
|
+
JS_METHOD_NAMES
|
|
986
|
+
end
|
|
987
|
+
|
|
952
988
|
def __js_call__(method, args)
|
|
953
989
|
case method
|
|
954
990
|
when "abort"
|
|
955
|
-
@signal.
|
|
991
|
+
@signal.__internal_mark_aborted__(args[0])
|
|
956
992
|
end
|
|
957
993
|
end
|
|
958
994
|
end
|
data/lib/dommy/event_source.rb
CHANGED
|
@@ -4,9 +4,9 @@ module Dommy
|
|
|
4
4
|
# `EventSource` (Server-Sent Events). Like `WebSocket`, dommy
|
|
5
5
|
# provides simulation seams instead of network IO:
|
|
6
6
|
#
|
|
7
|
-
# es.
|
|
8
|
-
# es.
|
|
9
|
-
# es.
|
|
7
|
+
# es.__test_simulate_open__
|
|
8
|
+
# es.__test_simulate_message__(data, event: "msg", id: "1")
|
|
9
|
+
# es.__test_simulate_error__
|
|
10
10
|
#
|
|
11
11
|
# Auto-opens on a microtask after construction, mirroring real
|
|
12
12
|
# browser behavior.
|
|
@@ -31,7 +31,7 @@ module Dommy
|
|
|
31
31
|
@with_credentials = !!(opts["withCredentials"] || opts[:withCredentials])
|
|
32
32
|
@inline_handlers = {}
|
|
33
33
|
|
|
34
|
-
@window.scheduler.queue_microtask(proc {
|
|
34
|
+
@window.scheduler.queue_microtask(proc { __test_simulate_open__ })
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def close
|
|
@@ -41,14 +41,14 @@ module Dommy
|
|
|
41
41
|
|
|
42
42
|
# --- Test seams ------------------------------------------------
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def __test_simulate_open__
|
|
45
45
|
return if @ready_state != CONNECTING
|
|
46
46
|
|
|
47
47
|
@ready_state = OPEN
|
|
48
48
|
dispatch_event(Event.new("open"))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def
|
|
51
|
+
def __test_simulate_message__(data, event: "message", id: nil, retry_ms: nil)
|
|
52
52
|
return if @ready_state != OPEN
|
|
53
53
|
|
|
54
54
|
payload = {"data" => data.to_s}
|
|
@@ -57,7 +57,7 @@ module Dommy
|
|
|
57
57
|
dispatch_event(MessageEvent.new(event.to_s, payload))
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def
|
|
60
|
+
def __test_simulate_error__
|
|
61
61
|
dispatch_event(Event.new("error"))
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -101,7 +101,7 @@ module Dommy
|
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def
|
|
104
|
+
def __internal_event_parent__
|
|
105
105
|
nil
|
|
106
106
|
end
|
|
107
107
|
|
data/lib/dommy/fetch.rb
CHANGED
|
@@ -227,8 +227,10 @@ module Dommy
|
|
|
227
227
|
rejected(err)
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
-
when "arrayBuffer"
|
|
231
|
-
immediate(@body)
|
|
230
|
+
when "arrayBuffer"
|
|
231
|
+
immediate(@body.bytes)
|
|
232
|
+
when "blob"
|
|
233
|
+
immediate(Blob.new([@body], "type" => @headers.__js_call__("get", ["content-type"]) || ""))
|
|
232
234
|
when "clone"
|
|
233
235
|
Response.new(
|
|
234
236
|
@window,
|
|
@@ -280,15 +282,21 @@ module Dommy
|
|
|
280
282
|
when "entries"
|
|
281
283
|
@hash.to_a
|
|
282
284
|
when "has"
|
|
283
|
-
|
|
285
|
+
# Match `get`'s case-insensitive lookup: try the raw name
|
|
286
|
+
# first, then the Title-Case canonical form. WHATWG defines
|
|
287
|
+
# header names as case-insensitive throughout the Headers API.
|
|
288
|
+
name = args[0].to_s
|
|
289
|
+
@hash.key?(name) || @hash.key?(Headers.canonical(name))
|
|
284
290
|
when "forEach"
|
|
285
|
-
#
|
|
291
|
+
# WHATWG: forEach(callback) — callback(value, key, headers).
|
|
292
|
+
# Pass `self` as the third argument so consumers that read
|
|
293
|
+
# `(_, _, h) => h.get("Foo")` work the same as in a browser.
|
|
286
294
|
cb = args[0]
|
|
287
295
|
@hash.each do |k, v|
|
|
288
296
|
if cb.respond_to?(:__js_call__)
|
|
289
|
-
cb.__js_call__("call", [v, k])
|
|
297
|
+
cb.__js_call__("call", [v, k, self])
|
|
290
298
|
elsif cb.respond_to?(:call)
|
|
291
|
-
cb.call(v, k)
|
|
299
|
+
cb.call(v, k, self)
|
|
292
300
|
end
|
|
293
301
|
end
|
|
294
302
|
|
data/lib/dommy/file_reader.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Dommy
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
def
|
|
117
|
+
def __internal_event_parent__
|
|
118
118
|
nil
|
|
119
119
|
end
|
|
120
120
|
|
|
@@ -145,8 +145,8 @@ module Dommy
|
|
|
145
145
|
|
|
146
146
|
# Returns the blob's raw bytes as a binary String.
|
|
147
147
|
def extract_raw(blob)
|
|
148
|
-
if blob.respond_to?(:
|
|
149
|
-
blob.
|
|
148
|
+
if blob.respond_to?(:__dommy_bytes__)
|
|
149
|
+
blob.__dommy_bytes__.to_s
|
|
150
150
|
else
|
|
151
151
|
blob.to_s
|
|
152
152
|
end
|
data/lib/dommy/form_data.rb
CHANGED
|
@@ -146,7 +146,7 @@ module Dommy
|
|
|
146
146
|
next if name.empty?
|
|
147
147
|
next if disabled?(el)
|
|
148
148
|
|
|
149
|
-
case el.
|
|
149
|
+
case el.__dommy_backend_node__.name
|
|
150
150
|
when "input"
|
|
151
151
|
collect_input(el, name)
|
|
152
152
|
when "select"
|
|
@@ -198,8 +198,6 @@ module Dommy
|
|
|
198
198
|
# File / Blob values pass through unchanged (multipart form
|
|
199
199
|
# encoding handles them); other values are stringified per spec.
|
|
200
200
|
return value if value.is_a?(Blob)
|
|
201
|
-
# Backward-compat: embedders' own file-marker objects.
|
|
202
|
-
return value if value.respond_to?(:__file_marker__)
|
|
203
201
|
return "" if value.nil?
|
|
204
202
|
|
|
205
203
|
value.to_s
|
data/lib/dommy/history.rb
CHANGED
|
@@ -58,14 +58,17 @@ module Dommy
|
|
|
58
58
|
|
|
59
59
|
def push(state, url)
|
|
60
60
|
@stack = @stack[0..@cursor]
|
|
61
|
-
@location.
|
|
62
|
-
|
|
61
|
+
@location.__internal_set_url__(url.to_s) if url
|
|
62
|
+
# WHATWG: pushState serializes the state via structured-clone
|
|
63
|
+
# so subsequent caller-side mutation of the original cannot
|
|
64
|
+
# affect history.state.
|
|
65
|
+
@stack << {state: Dommy.structured_clone(state), url: nil}
|
|
63
66
|
@cursor = @stack.size - 1
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def replace(state, url)
|
|
67
|
-
@location.
|
|
68
|
-
@stack[@cursor] = {state: state, url: nil}
|
|
70
|
+
@location.__internal_set_url__(url.to_s) if url
|
|
71
|
+
@stack[@cursor] = {state: Dommy.structured_clone(state), url: nil}
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
def go(delta)
|
|
@@ -46,9 +46,9 @@ module Dommy
|
|
|
46
46
|
return nil if key.empty?
|
|
47
47
|
|
|
48
48
|
to_a.find do |el|
|
|
49
|
-
next false unless el.respond_to?(:
|
|
49
|
+
next false unless el.respond_to?(:__dommy_backend_node__)
|
|
50
50
|
|
|
51
|
-
el.
|
|
51
|
+
el.__dommy_backend_node__["id"].to_s == key || el.__dommy_backend_node__["name"].to_s == key
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -125,7 +125,7 @@ module Dommy
|
|
|
125
125
|
# accepts either another option (insert before that node) or an
|
|
126
126
|
# integer index. Strings/`null` append.
|
|
127
127
|
def add(option, before = nil)
|
|
128
|
-
return nil unless option.respond_to?(:
|
|
128
|
+
return nil unless option.respond_to?(:__dommy_backend_node__)
|
|
129
129
|
|
|
130
130
|
case before
|
|
131
131
|
when nil
|
|
@@ -134,7 +134,7 @@ module Dommy
|
|
|
134
134
|
anchor = item(before)
|
|
135
135
|
anchor ? @owner.insert_before(option, anchor) : @owner.append_child(option)
|
|
136
136
|
else
|
|
137
|
-
if before.respond_to?(:
|
|
137
|
+
if before.respond_to?(:__dommy_backend_node__)
|
|
138
138
|
@owner.insert_before(option, before)
|
|
139
139
|
else
|
|
140
140
|
@owner.append_child(option)
|