dommy 0.5.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 +31 -13
- data/lib/dommy/animation.rb +288 -0
- 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 +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +396 -0
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +190 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +356 -53
- data/lib/dommy/event.rb +431 -25
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +76 -6
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +82 -0
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +130 -67
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +4 -4
- data/lib/dommy/internal/idna.rb +443 -0
- data/lib/dommy/internal/idna_data.rb +10379 -0
- data/lib/dommy/internal/ipv4_parser.rb +78 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/observable_callback.rb +25 -0
- data/lib/dommy/internal/punycode.rb +202 -0
- data/lib/dommy/internal/range_text_serializer.rb +72 -0
- data/lib/dommy/internal/reflected_attributes.rb +45 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +8 -142
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
- data/lib/dommy/navigator.rb +365 -5
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +89 -0
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +146 -0
- data/lib/dommy/performance_observer.rb +55 -0
- data/lib/dommy/range.rb +597 -0
- data/lib/dommy/resize_observer.rb +53 -0
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +274 -29
- data/lib/dommy/url_pattern.rb +144 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +209 -0
- data/lib/dommy/window.rb +369 -0
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +438 -0
- data/lib/dommy.rb +43 -5
- metadata +44 -29
- data/lib/dommy/world.rb +0 -209
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `WebSocket` polyfill. Real implementations open a TCP-then-frame
|
|
5
|
+
# connection; dommy exposes an in-memory transport tests drive via
|
|
6
|
+
# the `__*` seams:
|
|
7
|
+
#
|
|
8
|
+
# ws.__test_simulate_open__ — fires `open`
|
|
9
|
+
# ws.__test_simulate_message__(data) — fires `message`
|
|
10
|
+
# ws.__test_simulate_close__(code, reason) — fires `close`
|
|
11
|
+
# ws.__test_simulate_error__ — fires `error`
|
|
12
|
+
# ws.__test_sent_messages__ — array of sent payloads
|
|
13
|
+
#
|
|
14
|
+
# By default a `new WebSocket(url)` auto-opens via microtask so the
|
|
15
|
+
# common pattern (`ws.onopen = ...; ws.send(...)`) works without
|
|
16
|
+
# extra setup.
|
|
17
|
+
#
|
|
18
|
+
# Spec: https://websockets.spec.whatwg.org/
|
|
19
|
+
class WebSocket
|
|
20
|
+
include EventTarget
|
|
21
|
+
|
|
22
|
+
CONNECTING = 0
|
|
23
|
+
OPEN = 1
|
|
24
|
+
CLOSING = 2
|
|
25
|
+
CLOSED = 3
|
|
26
|
+
|
|
27
|
+
INLINE_HANDLERS = %w[open message close error].freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :url, :protocol, :ready_state, :buffered_amount, :extensions
|
|
30
|
+
attr_accessor :binary_type
|
|
31
|
+
|
|
32
|
+
def initialize(window, url, protocols = nil)
|
|
33
|
+
@window = window
|
|
34
|
+
@url = url.to_s
|
|
35
|
+
@ready_state = CONNECTING
|
|
36
|
+
@buffered_amount = 0
|
|
37
|
+
@extensions = ""
|
|
38
|
+
@binary_type = "blob"
|
|
39
|
+
@protocol = Array(protocols).first.to_s
|
|
40
|
+
@sent_messages = []
|
|
41
|
+
@inline_handlers = {}
|
|
42
|
+
|
|
43
|
+
# Auto-open via microtask unless tests disable.
|
|
44
|
+
auto_open = window.globals["__ws_auto_open__"]
|
|
45
|
+
@window.scheduler.queue_microtask(proc { __test_simulate_open__ }) unless auto_open == false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def send(data)
|
|
49
|
+
raise Error, "WebSocket not OPEN" if @ready_state != OPEN
|
|
50
|
+
|
|
51
|
+
@sent_messages << data
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def close(code = 1000, reason = "")
|
|
56
|
+
return if @ready_state == CLOSED || @ready_state == CLOSING
|
|
57
|
+
|
|
58
|
+
@ready_state = CLOSING
|
|
59
|
+
@window.scheduler.queue_microtask(proc { __test_simulate_close__(code, reason) })
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# --- Test seams ------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def __test_sent_messages__
|
|
66
|
+
@sent_messages.dup
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def __test_simulate_open__
|
|
70
|
+
return if @ready_state != CONNECTING
|
|
71
|
+
|
|
72
|
+
@ready_state = OPEN
|
|
73
|
+
dispatch_event(Event.new("open"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def __test_simulate_message__(data)
|
|
77
|
+
return if @ready_state != OPEN
|
|
78
|
+
|
|
79
|
+
dispatch_event(MessageEvent.new("message", "data" => data))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def __test_simulate_close__(code = 1000, reason = "", was_clean: true)
|
|
83
|
+
@ready_state = CLOSED
|
|
84
|
+
dispatch_event(
|
|
85
|
+
CloseEvent.new(
|
|
86
|
+
"close",
|
|
87
|
+
"code" => code,
|
|
88
|
+
"reason" => reason,
|
|
89
|
+
"wasClean" => was_clean
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def __test_simulate_error__
|
|
95
|
+
dispatch_event(Event.new("error"))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# --- JS bridge -------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def __js_get__(key)
|
|
101
|
+
case key
|
|
102
|
+
when "url"
|
|
103
|
+
@url
|
|
104
|
+
when "readyState"
|
|
105
|
+
@ready_state
|
|
106
|
+
when "bufferedAmount"
|
|
107
|
+
@buffered_amount
|
|
108
|
+
when "extensions"
|
|
109
|
+
@extensions
|
|
110
|
+
when "protocol"
|
|
111
|
+
@protocol
|
|
112
|
+
when "binaryType"
|
|
113
|
+
@binary_type
|
|
114
|
+
when "CONNECTING"
|
|
115
|
+
CONNECTING
|
|
116
|
+
when "OPEN"
|
|
117
|
+
OPEN
|
|
118
|
+
when "CLOSING"
|
|
119
|
+
CLOSING
|
|
120
|
+
when "CLOSED"
|
|
121
|
+
CLOSED
|
|
122
|
+
else
|
|
123
|
+
@inline_handlers[inline_event_for(key)]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def __js_set__(key, value)
|
|
128
|
+
case key
|
|
129
|
+
when "binaryType"
|
|
130
|
+
@binary_type = value.to_s
|
|
131
|
+
else
|
|
132
|
+
event = inline_event_for(key)
|
|
133
|
+
set_inline_handler(event, value) if event
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def __js_call__(method, args)
|
|
140
|
+
case method
|
|
141
|
+
when "send"
|
|
142
|
+
send(args[0])
|
|
143
|
+
when "close"
|
|
144
|
+
close(args[0] || 1000, args[1] || "")
|
|
145
|
+
when "addEventListener"
|
|
146
|
+
add_event_listener(args[0], args[1], args[2])
|
|
147
|
+
when "removeEventListener"
|
|
148
|
+
remove_event_listener(args[0], args[1])
|
|
149
|
+
when "dispatchEvent"
|
|
150
|
+
dispatch_event(args[0])
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def __internal_event_parent__
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class Error < StandardError
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
INLINE_EVENT_MAP = INLINE_HANDLERS
|
|
164
|
+
.each_with_object({}) do |name, h|
|
|
165
|
+
h["on#{name}"] = name
|
|
166
|
+
end
|
|
167
|
+
.freeze
|
|
168
|
+
|
|
169
|
+
def inline_event_for(key)
|
|
170
|
+
INLINE_EVENT_MAP[key.to_s]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def set_inline_handler(event, handler)
|
|
174
|
+
previous = @inline_handlers[event]
|
|
175
|
+
remove_event_listener(event, previous) if previous
|
|
176
|
+
if handler.nil?
|
|
177
|
+
@inline_handlers.delete(event)
|
|
178
|
+
else
|
|
179
|
+
add_event_listener(event, handler)
|
|
180
|
+
@inline_handlers[event] = handler
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# `CloseEvent` — payload for the `close` event on WebSocket.
|
|
186
|
+
class CloseEvent < Event
|
|
187
|
+
def initialize(type, init = nil)
|
|
188
|
+
super
|
|
189
|
+
@code = (read_init(init, "code") || 1005).to_i
|
|
190
|
+
@reason = (read_init(init, "reason") || "").to_s
|
|
191
|
+
@was_clean = !!read_init(init, "wasClean")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
attr_reader :code, :reason, :was_clean
|
|
195
|
+
|
|
196
|
+
def __js_get__(key)
|
|
197
|
+
case key
|
|
198
|
+
when "code"
|
|
199
|
+
@code
|
|
200
|
+
when "reason"
|
|
201
|
+
@reason
|
|
202
|
+
when "wasClean"
|
|
203
|
+
@was_clean
|
|
204
|
+
else
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
data/lib/dommy/window.rb
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
# Dommy — a happy-dom-style DOM polyfill in pure Ruby. Backbone is
|
|
7
|
+
# Nokogiri::HTML5 plus a small scheduler/event-loop layer.
|
|
8
|
+
#
|
|
9
|
+
# Two views into the same objects:
|
|
10
|
+
# - Public Ruby API (snake_case methods like `text_content`,
|
|
11
|
+
# `append_child`) for CRuby users writing tests against rendered
|
|
12
|
+
# HTML.
|
|
13
|
+
# - `__js_get__` / `__js_set__` / `__js_call__` / `__js_new__`
|
|
14
|
+
# bridge protocol for JS bridge embedders — dispatches into the
|
|
15
|
+
# same underlying Ruby methods.
|
|
16
|
+
module Dommy
|
|
17
|
+
# The browser global. `JS.global` from inside wasm resolves to this.
|
|
18
|
+
# Property access (`JS.global[:document]`, `JS.global[:console]`) is
|
|
19
|
+
# routed through `#__js_get__`. Method calls (`JS.global.call(:foo)`)
|
|
20
|
+
# are routed through `#__js_call__`.
|
|
21
|
+
class Window
|
|
22
|
+
include EventTarget
|
|
23
|
+
|
|
24
|
+
attr_reader :document, :scheduler, :location, :globals, :custom_elements, :navigator
|
|
25
|
+
|
|
26
|
+
def initialize(host = nil, nokogiri_doc: nil)
|
|
27
|
+
@host = host
|
|
28
|
+
@scheduler = Scheduler.new
|
|
29
|
+
@event_ctor = Bridge::Constructor.new { |args| Event.new(args[0], args[1]) }
|
|
30
|
+
@custom_event_ctor = Bridge::Constructor.new { |args| CustomEvent.new(args[0], args[1]) }
|
|
31
|
+
@mouse_event_ctor = Bridge::Constructor.new { |args| MouseEvent.new(args[0], args[1]) }
|
|
32
|
+
@keyboard_event_ctor = Bridge::Constructor.new { |args| KeyboardEvent.new(args[0], args[1]) }
|
|
33
|
+
@event_target_ctor = Bridge::Constructor.new { |_args| StandaloneEventTarget.new }
|
|
34
|
+
@error_ctor = Bridge::Constructor.new { |args| ErrorValue.new(args[0]) }
|
|
35
|
+
@promise_ctor = Bridge::PromiseConstructor.new(self)
|
|
36
|
+
@mutation_observer_ctor = Bridge::Constructor.new { |args| MutationObserver.new(self, args[0]) }
|
|
37
|
+
@abort_controller_ctor = Bridge::Constructor.new { |_args| AbortController.new }
|
|
38
|
+
@blob_ctor = Bridge::Constructor.new { |args| Blob.new(args[0] || [], args[1] || {}) }
|
|
39
|
+
@file_ctor = Bridge::Constructor.new { |args| File.new(args[0] || [], args[1].to_s, args[2] || {}) }
|
|
40
|
+
@file_list_ctor = Bridge::Constructor.new { |args| FileList.new(args[0] || []) }
|
|
41
|
+
@data_transfer_ctor = Bridge::Constructor.new { |args|
|
|
42
|
+
opts = args[0] || {}
|
|
43
|
+
DataTransfer.new(
|
|
44
|
+
files: opts["files"] || opts[:files] || [],
|
|
45
|
+
data: opts["data"] || opts[:data] || {}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
@drag_event_ctor = Bridge::Constructor.new { |args| DragEvent.new(args[0], args[1]) }
|
|
49
|
+
@input_event_ctor = Bridge::Constructor.new { |args| InputEvent.new(args[0], args[1]) }
|
|
50
|
+
@pointer_event_ctor = Bridge::Constructor.new { |args| PointerEvent.new(args[0], args[1]) }
|
|
51
|
+
@progress_event_ctor = Bridge::Constructor.new { |args| ProgressEvent.new(args[0], args[1]) }
|
|
52
|
+
@touch_ctor = Bridge::Constructor.new { |args| Touch.new(args[0] || {}) }
|
|
53
|
+
@touch_event_ctor = Bridge::Constructor.new { |args| TouchEvent.new(args[0], args[1]) }
|
|
54
|
+
@clipboard_event_ctor = Bridge::Constructor.new { |args| ClipboardEvent.new(args[0], args[1]) }
|
|
55
|
+
@composition_event_ctor = Bridge::Constructor.new { |args| CompositionEvent.new(args[0], args[1]) }
|
|
56
|
+
@wheel_event_ctor = Bridge::Constructor.new { |args| WheelEvent.new(args[0], args[1]) }
|
|
57
|
+
@focus_event_ctor = Bridge::Constructor.new { |args| FocusEvent.new(args[0], args[1]) }
|
|
58
|
+
@before_unload_event_ctor = Bridge::Constructor.new { |args|
|
|
59
|
+
BeforeUnloadEvent.new(args[0] || "beforeunload", args[1])
|
|
60
|
+
}
|
|
61
|
+
win_ref = self
|
|
62
|
+
@animation_ctor = Bridge::Constructor.new { |args| Animation.new(args[0], args[1], window: win_ref) }
|
|
63
|
+
@keyframe_effect_ctor = Bridge::Constructor.new { |args| KeyframeEffect.new(args[0], args[1] || [], args[2]) }
|
|
64
|
+
@crypto = Crypto.new(self)
|
|
65
|
+
@text_encoder_ctor = Bridge::Constructor.new { |_args| TextEncoder.new }
|
|
66
|
+
@text_decoder_ctor = Bridge::Constructor.new { |args| TextDecoder.new(args[0] || "utf-8", args[1]) }
|
|
67
|
+
@intersection_observer_ctor = Bridge::Constructor.new { |args| IntersectionObserver.new(args[0], args[1]) }
|
|
68
|
+
@resize_observer_ctor = Bridge::Constructor.new { |args| ResizeObserver.new(args[0]) }
|
|
69
|
+
@performance_observer_ctor = Bridge::Constructor.new { |args| PerformanceObserver.new(args[0]) }
|
|
70
|
+
@request_ctor = Bridge::Constructor.new { |args| Request.new(args[0], args[1]) }
|
|
71
|
+
xhr_win_ref = self
|
|
72
|
+
@xhr_ctor = Bridge::Constructor.new { |_args| XMLHttpRequest.new(xhr_win_ref) }
|
|
73
|
+
@file_reader_ctor = Bridge::Constructor.new { |_args| FileReader.new(xhr_win_ref) }
|
|
74
|
+
@message_channel_ctor = Bridge::Constructor.new { |_args| MessageChannel.new(xhr_win_ref) }
|
|
75
|
+
@broadcast_channel_ctor = Bridge::Constructor.new { |args| BroadcastChannel.new(xhr_win_ref, args[0]) }
|
|
76
|
+
@web_socket_ctor = Bridge::Constructor.new { |args| WebSocket.new(xhr_win_ref, args[0], args[1]) }
|
|
77
|
+
@event_source_ctor = Bridge::Constructor.new { |args| EventSource.new(xhr_win_ref, args[0], args[1]) }
|
|
78
|
+
@notification_ctor = Bridge::Constructor.new { |args| Notification.new(xhr_win_ref, args[0], args[1]) }
|
|
79
|
+
@notification_ctor.define_class_method("requestPermission") do |args|
|
|
80
|
+
Notification.request_permission(xhr_win_ref, args[0])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@worker_ctor = Bridge::Constructor.new { |args| Worker.new(xhr_win_ref, args[0], args[1]) }
|
|
84
|
+
@readable_stream_ctor = Bridge::Constructor.new { |args| ReadableStream.new(xhr_win_ref, args[0]) }
|
|
85
|
+
@writable_stream_ctor = Bridge::Constructor.new { |args| WritableStream.new(xhr_win_ref, args[0]) }
|
|
86
|
+
@transform_stream_ctor = Bridge::Constructor.new { |args| TransformStream.new(xhr_win_ref, args[0]) }
|
|
87
|
+
@text_encoder_stream_ctor = Bridge::Constructor.new { |_args| TextEncoderStream.new(xhr_win_ref) }
|
|
88
|
+
@text_decoder_stream_ctor = Bridge::Constructor.new { |args|
|
|
89
|
+
TextDecoderStream.new(xhr_win_ref, args[0] || "utf-8", args[1])
|
|
90
|
+
}
|
|
91
|
+
@compression_stream_ctor = Bridge::Constructor.new { |args| CompressionStream.new(xhr_win_ref, args[0]) }
|
|
92
|
+
@decompression_stream_ctor = Bridge::Constructor.new { |args| DecompressionStream.new(xhr_win_ref, args[0]) }
|
|
93
|
+
@url_pattern_ctor = Bridge::Constructor.new { |args| URLPattern.new(args[0], args[1]) }
|
|
94
|
+
@cookie_store = CookieStore.new(xhr_win_ref)
|
|
95
|
+
|
|
96
|
+
@range_ctor = Bridge::Constructor.new { |_args| Range.new(@document) }
|
|
97
|
+
@local_storage = Storage.new
|
|
98
|
+
@session_storage = Storage.new
|
|
99
|
+
@location = Location.new(self)
|
|
100
|
+
@history = History.new(self, @location)
|
|
101
|
+
@url_ctor = Bridge::Constructor.new { |args| URL.new(args[0], args[1]) }
|
|
102
|
+
@url_ctor.define_class_method("createObjectURL") { |args| URL.create_object_url(args[0]) }
|
|
103
|
+
@url_ctor.define_class_method("revokeObjectURL") { |args| URL.revoke_object_url(args[0]) }
|
|
104
|
+
@url_ctor.define_class_method("parse") { |args| URL.parse(args[0], args[1]) }
|
|
105
|
+
@url_ctor.define_class_method("canParse") { |args| URL.can_parse(args[0], args[1]) }
|
|
106
|
+
# `JS.global[:__some_key__] = ...` from user code lands here.
|
|
107
|
+
# Test code uses this for stub installation (e.g. a custom
|
|
108
|
+
# `__fetch_stub__`); production code stays on the typed
|
|
109
|
+
# accessors above. We keep it last in the read fallback to
|
|
110
|
+
# avoid shadowing intentional getters.
|
|
111
|
+
@globals = {}
|
|
112
|
+
@document = Document.new(host, nokogiri_doc: nokogiri_doc)
|
|
113
|
+
@document.default_view = self
|
|
114
|
+
@custom_elements = CustomElementRegistry.new(self)
|
|
115
|
+
@navigator = Navigator.new(self)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Bridge protocol: respond to a JS-style property read by name.
|
|
119
|
+
# Returns either a Ruby primitive (Integer / String / true / false /
|
|
120
|
+
# nil), a Hash/Array (for JS object/array literals), or a Dom::*
|
|
121
|
+
# instance for live DOM/BOM objects.
|
|
122
|
+
#
|
|
123
|
+
# Anything outside the surface we've explicitly polyfilled returns
|
|
124
|
+
# nil (= JS undefined). Spec failures here are the signal to widen
|
|
125
|
+
# the surface in a future session.
|
|
126
|
+
def __js_get__(key)
|
|
127
|
+
case key
|
|
128
|
+
when "document"
|
|
129
|
+
@document
|
|
130
|
+
when "Event"
|
|
131
|
+
@event_ctor
|
|
132
|
+
when "CustomEvent"
|
|
133
|
+
@custom_event_ctor
|
|
134
|
+
when "MouseEvent"
|
|
135
|
+
@mouse_event_ctor
|
|
136
|
+
when "KeyboardEvent"
|
|
137
|
+
@keyboard_event_ctor
|
|
138
|
+
when "EventTarget"
|
|
139
|
+
@event_target_ctor
|
|
140
|
+
when "Error"
|
|
141
|
+
@error_ctor
|
|
142
|
+
when "Promise"
|
|
143
|
+
@promise_ctor
|
|
144
|
+
when "MutationObserver"
|
|
145
|
+
@mutation_observer_ctor
|
|
146
|
+
when "AbortController"
|
|
147
|
+
@abort_controller_ctor
|
|
148
|
+
when "Blob"
|
|
149
|
+
@blob_ctor
|
|
150
|
+
when "File"
|
|
151
|
+
@file_ctor
|
|
152
|
+
when "FileList"
|
|
153
|
+
@file_list_ctor
|
|
154
|
+
when "DataTransfer"
|
|
155
|
+
@data_transfer_ctor
|
|
156
|
+
when "DragEvent"
|
|
157
|
+
@drag_event_ctor
|
|
158
|
+
when "InputEvent"
|
|
159
|
+
@input_event_ctor
|
|
160
|
+
when "PointerEvent"
|
|
161
|
+
@pointer_event_ctor
|
|
162
|
+
when "ProgressEvent"
|
|
163
|
+
@progress_event_ctor
|
|
164
|
+
when "Touch"
|
|
165
|
+
@touch_ctor
|
|
166
|
+
when "TouchEvent"
|
|
167
|
+
@touch_event_ctor
|
|
168
|
+
when "ClipboardEvent"
|
|
169
|
+
@clipboard_event_ctor
|
|
170
|
+
when "CompositionEvent"
|
|
171
|
+
@composition_event_ctor
|
|
172
|
+
when "WheelEvent"
|
|
173
|
+
@wheel_event_ctor
|
|
174
|
+
when "FocusEvent"
|
|
175
|
+
@focus_event_ctor
|
|
176
|
+
when "BeforeUnloadEvent"
|
|
177
|
+
@before_unload_event_ctor
|
|
178
|
+
when "Animation"
|
|
179
|
+
@animation_ctor
|
|
180
|
+
when "KeyframeEffect"
|
|
181
|
+
@keyframe_effect_ctor
|
|
182
|
+
when "crypto"
|
|
183
|
+
@crypto
|
|
184
|
+
when "TextEncoder"
|
|
185
|
+
@text_encoder_ctor
|
|
186
|
+
when "TextDecoder"
|
|
187
|
+
@text_decoder_ctor
|
|
188
|
+
when "IntersectionObserver"
|
|
189
|
+
@intersection_observer_ctor
|
|
190
|
+
when "ResizeObserver"
|
|
191
|
+
@resize_observer_ctor
|
|
192
|
+
when "PerformanceObserver"
|
|
193
|
+
@performance_observer_ctor
|
|
194
|
+
when "Request"
|
|
195
|
+
@request_ctor
|
|
196
|
+
when "XMLHttpRequest"
|
|
197
|
+
@xhr_ctor
|
|
198
|
+
when "FileReader"
|
|
199
|
+
@file_reader_ctor
|
|
200
|
+
when "MessageChannel"
|
|
201
|
+
@message_channel_ctor
|
|
202
|
+
when "BroadcastChannel"
|
|
203
|
+
@broadcast_channel_ctor
|
|
204
|
+
when "WebSocket"
|
|
205
|
+
@web_socket_ctor
|
|
206
|
+
when "EventSource"
|
|
207
|
+
@event_source_ctor
|
|
208
|
+
when "Notification"
|
|
209
|
+
@notification_ctor
|
|
210
|
+
when "Worker"
|
|
211
|
+
@worker_ctor
|
|
212
|
+
when "ReadableStream"
|
|
213
|
+
@readable_stream_ctor
|
|
214
|
+
when "WritableStream"
|
|
215
|
+
@writable_stream_ctor
|
|
216
|
+
when "TransformStream"
|
|
217
|
+
@transform_stream_ctor
|
|
218
|
+
when "TextEncoderStream"
|
|
219
|
+
@text_encoder_stream_ctor
|
|
220
|
+
when "TextDecoderStream"
|
|
221
|
+
@text_decoder_stream_ctor
|
|
222
|
+
when "CompressionStream"
|
|
223
|
+
@compression_stream_ctor
|
|
224
|
+
when "DecompressionStream"
|
|
225
|
+
@decompression_stream_ctor
|
|
226
|
+
when "URLPattern"
|
|
227
|
+
@url_pattern_ctor
|
|
228
|
+
when "cookieStore"
|
|
229
|
+
@cookie_store
|
|
230
|
+
when "Range"
|
|
231
|
+
@range_ctor
|
|
232
|
+
# handled by Symbol sentinel
|
|
233
|
+
when "console"
|
|
234
|
+
:console
|
|
235
|
+
# likewise
|
|
236
|
+
when "Object"
|
|
237
|
+
:object_ctor
|
|
238
|
+
when "Array"
|
|
239
|
+
:array_ctor
|
|
240
|
+
when "JSON"
|
|
241
|
+
:json_ctor
|
|
242
|
+
when "performance"
|
|
243
|
+
@performance ||= Performance.new(self)
|
|
244
|
+
when "localStorage"
|
|
245
|
+
@local_storage
|
|
246
|
+
when "sessionStorage"
|
|
247
|
+
@session_storage
|
|
248
|
+
when "location"
|
|
249
|
+
@location
|
|
250
|
+
when "history"
|
|
251
|
+
@history
|
|
252
|
+
when "URL"
|
|
253
|
+
@url_ctor
|
|
254
|
+
when "fetch"
|
|
255
|
+
FetchFn.new(self)
|
|
256
|
+
when "customElements"
|
|
257
|
+
@custom_elements
|
|
258
|
+
when "navigator"
|
|
259
|
+
@navigator
|
|
260
|
+
else
|
|
261
|
+
@globals[key]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def __js_set__(key, value)
|
|
266
|
+
# Stash arbitrary keys for later reads (e.g.
|
|
267
|
+
# `JS.global[:__fetchy_stub__] = map`).
|
|
268
|
+
@globals[key] = value
|
|
269
|
+
# The Fetchy spec's `install_fetch_stub` resets `__fetch_count__`
|
|
270
|
+
# to 0 inside its JS installer (`globalThis.__fetch_count__ = 0;
|
|
271
|
+
# globalThis.fetch = ...`). Our polyfill ignores raw JS, so we
|
|
272
|
+
# piggy-back on the stub assignment to perform the same reset
|
|
273
|
+
# — without it the count accumulates across tests in one VM run.
|
|
274
|
+
@globals["__fetch_count__"] = 0 if %w[__fetchy_stub__ __resource_fetch_stub__ __inject_fetch_stub__].include?(key)
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
279
|
+
JS_METHOD_NAMES = %w[
|
|
280
|
+
fetch encodeURIComponent decodeURIComponent addEventListener removeEventListener
|
|
281
|
+
dispatchEvent setTimeout clearTimeout setInterval clearInterval requestAnimationFrame
|
|
282
|
+
cancelAnimationFrame queueMicrotask requestIdleCallback cancelIdleCallback
|
|
283
|
+
structuredClone matchMedia getComputedStyle
|
|
284
|
+
].freeze
|
|
285
|
+
def __js_method_names__
|
|
286
|
+
JS_METHOD_NAMES
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def __js_call__(method, args)
|
|
290
|
+
case method
|
|
291
|
+
when "fetch"
|
|
292
|
+
FetchFn.new(self).__js_call__("call", args)
|
|
293
|
+
when "encodeURIComponent"
|
|
294
|
+
# JS spec encoding: percent-encode anything except
|
|
295
|
+
# `A-Za-z0-9 - _ . ! ~ * ' ( )`. Ruby's `CGI.escape` uses
|
|
296
|
+
# `+` for space; ERB::Util.url_encode matches JS behavior.
|
|
297
|
+
ERB::Util.url_encode(args[0].to_s)
|
|
298
|
+
when "decodeURIComponent"
|
|
299
|
+
CGI.unescape(args[0].to_s)
|
|
300
|
+
when "addEventListener"
|
|
301
|
+
add_event_listener(args[0], args[1], args[2])
|
|
302
|
+
when "removeEventListener"
|
|
303
|
+
remove_event_listener(args[0], args[1])
|
|
304
|
+
when "dispatchEvent"
|
|
305
|
+
dispatch_event(args[0])
|
|
306
|
+
when "setTimeout"
|
|
307
|
+
@scheduler.set_timeout(args[0], args[1] || 0)
|
|
308
|
+
when "clearTimeout"
|
|
309
|
+
@scheduler.clear_timeout(args[0])
|
|
310
|
+
when "setInterval"
|
|
311
|
+
@scheduler.set_interval(args[0], args[1] || 0)
|
|
312
|
+
when "clearInterval"
|
|
313
|
+
@scheduler.clear_interval(args[0])
|
|
314
|
+
when "requestAnimationFrame"
|
|
315
|
+
@scheduler.request_animation_frame(args[0])
|
|
316
|
+
when "cancelAnimationFrame"
|
|
317
|
+
@scheduler.cancel_animation_frame(args[0])
|
|
318
|
+
when "queueMicrotask"
|
|
319
|
+
@scheduler.queue_microtask(args[0])
|
|
320
|
+
when "requestIdleCallback"
|
|
321
|
+
# WHATWG `requestIdleCallback` — no real idle period in
|
|
322
|
+
# dommy, so we model it as a deferred setTimeout. The
|
|
323
|
+
# callback receives an `IdleDeadline`-shaped Hash.
|
|
324
|
+
@scheduler.set_timeout(
|
|
325
|
+
proc {
|
|
326
|
+
args[0].respond_to?(:__js_call__) ? args[0].__js_call__(
|
|
327
|
+
"call",
|
|
328
|
+
[{"timeRemaining" => 50.0, "didTimeout" => false}]
|
|
329
|
+
) : args[0].call({"timeRemaining" => 50.0, "didTimeout" => false})
|
|
330
|
+
},
|
|
331
|
+
(args[1].is_a?(Hash) && args[1]["timeout"]) || 0
|
|
332
|
+
)
|
|
333
|
+
when "cancelIdleCallback"
|
|
334
|
+
@scheduler.clear_timeout(args[0])
|
|
335
|
+
when "structuredClone"
|
|
336
|
+
Dommy.structured_clone(args[0])
|
|
337
|
+
when "matchMedia"
|
|
338
|
+
MediaQueryList.new(self, args[0].to_s)
|
|
339
|
+
when "getComputedStyle"
|
|
340
|
+
# No CSS engine — return the element's inline style. That
|
|
341
|
+
# covers `getComputedStyle(el).getPropertyValue("color")` for
|
|
342
|
+
# values the test set inline via `el.style.color = "..."`.
|
|
343
|
+
target = args[0]
|
|
344
|
+
target.respond_to?(:style) ? target.style : nil
|
|
345
|
+
else
|
|
346
|
+
# Additional window-level methods (fetch, location, history,
|
|
347
|
+
# Promise, MutationObserver, etc.) arrive in later sessions.
|
|
348
|
+
nil
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def __internal_event_parent__
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Called by History#go and Location.href= to fire popstate /
|
|
357
|
+
# hashchange events. Listeners registered on the Window via
|
|
358
|
+
# `addEventListener("popstate"|"hashchange", cb)` receive them.
|
|
359
|
+
def fire_popstate(state)
|
|
360
|
+
event = CustomEvent.new("popstate", "detail" => state)
|
|
361
|
+
dispatch_event(event)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def fire_hashchange(old_hash, new_hash)
|
|
365
|
+
event = CustomEvent.new("hashchange", "detail" => {"oldURL" => old_hash, "newURL" => new_hash})
|
|
366
|
+
dispatch_event(event)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|