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
data/lib/dommy/url.rb ADDED
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "cgi"
5
+
6
+ module Dommy
7
+ # `URL` — WHATWG-style URL parsing. Public API mirrors the JS class:
8
+ #
9
+ # u = Dommy::URL.new("https://x.test/a/b?k=v#h")
10
+ # u.protocol # "https:"
11
+ # u.host # "x.test"
12
+ # u.pathname # "/a/b"
13
+ # u.search # "?k=v"
14
+ # u.hash # "#h"
15
+ # u.search_params.get("k") # "v"
16
+ #
17
+ # Construction with a base URL is supported for relative inputs:
18
+ # Dommy::URL.new("/a", "https://x.test").href
19
+ # # => "https://x.test/a"
20
+ #
21
+ # Internally backed by Ruby's URI library — good enough for the
22
+ # common test cases. Edge cases that URI rejects raise
23
+ # `DOMException::SyntaxError` (called `TypeError` in JS but Dommy
24
+ # uses the closest WHATWG name).
25
+ class URL
26
+ # Registry of Blob URLs created via `URL.createObjectURL(blob)`.
27
+ # Process-wide because the spec scopes them to the document/window
28
+ # lifecycle, but Dommy is a single-process test harness.
29
+ @blob_urls = {}
30
+
31
+ class << self
32
+ # Create a unique blob: URL that resolves back to `blob` via
33
+ # `URL.__resolve_blob_url__(url)`. Returns nil for non-Blob input.
34
+ def create_object_url(blob)
35
+ return nil unless blob.is_a?(Blob)
36
+
37
+ id = "%032x" % rand(2 ** 128)
38
+ url = "blob:dommy/#{id}"
39
+ @blob_urls[url] = blob
40
+ url
41
+ end
42
+
43
+ alias createObjectURL create_object_url
44
+
45
+ # Revoke a previously-created blob URL. No-op for unknown URLs,
46
+ # matching the spec.
47
+ def revoke_object_url(url)
48
+ @blob_urls.delete(url.to_s)
49
+ nil
50
+ end
51
+
52
+ alias revokeObjectURL revoke_object_url
53
+
54
+ # Resolve a blob: URL back to its Blob, or nil if revoked / unknown.
55
+ # Internal — used by fetch / XHR implementations that load blob URLs.
56
+ def __resolve_blob_url__(url)
57
+ @blob_urls[url.to_s]
58
+ end
59
+
60
+ # Test seam: drop all registered blob URLs.
61
+ def __reset_blob_urls__
62
+ @blob_urls.clear
63
+ end
64
+ end
65
+
66
+ attr_reader :search_params
67
+
68
+ def initialize(input, base = nil)
69
+ raw = parse_with_base(input, base)
70
+ @uri = raw
71
+ @search_params = URLSearchParams.new(raw.query.to_s, owner: self)
72
+ end
73
+
74
+ def href
75
+ build_href
76
+ end
77
+
78
+ def href=(value)
79
+ raw = parse_with_base(value.to_s, nil)
80
+ @uri = raw
81
+ @search_params.__replace__(raw.query.to_s)
82
+ build_href
83
+ end
84
+
85
+ def protocol
86
+ @uri.scheme ? "#{@uri.scheme}:" : ""
87
+ end
88
+
89
+ def protocol=(value)
90
+ s = value.to_s.sub(/:$/, "")
91
+ @uri.scheme = s
92
+ end
93
+
94
+ def host
95
+ port = @uri.port
96
+ default = @uri.default_port
97
+ hostpart = @uri.host.to_s
98
+ return hostpart if port.nil? || port == default
99
+
100
+ "#{hostpart}:#{port}"
101
+ end
102
+
103
+ def host=(value)
104
+ h, p = value.to_s.split(":", 2)
105
+ @uri.host = h
106
+ @uri.port = p.to_i if p
107
+ end
108
+
109
+ def hostname
110
+ @uri.host.to_s
111
+ end
112
+
113
+ def hostname=(value)
114
+ @uri.host = value.to_s
115
+ end
116
+
117
+ def port
118
+ return "" if @uri.port.nil? || @uri.port == @uri.default_port
119
+
120
+ @uri.port.to_s
121
+ end
122
+
123
+ def port=(value)
124
+ @uri.port = value.to_s.empty? ? nil : value.to_i
125
+ end
126
+
127
+ def pathname
128
+ @uri.path.to_s
129
+ end
130
+
131
+ def pathname=(value)
132
+ v = value.to_s
133
+ v = "/#{v}" if !v.start_with?("/") && !v.empty?
134
+ @uri.path = v
135
+ end
136
+
137
+ def search
138
+ q = @search_params.to_s
139
+ q.empty? ? "" : "?#{q}"
140
+ end
141
+
142
+ def search=(value)
143
+ q = value.to_s.sub(/^\?/, "")
144
+ @search_params.__replace__(q)
145
+ sync_uri_query
146
+ end
147
+
148
+ def hash
149
+ f = @uri.fragment.to_s
150
+ f.empty? ? "" : "##{f}"
151
+ end
152
+
153
+ def hash=(value)
154
+ f = value.to_s.sub(/^#/, "")
155
+ @uri.fragment = f.empty? ? nil : f
156
+ end
157
+
158
+ def origin
159
+ return "null" unless @uri.scheme && @uri.host
160
+
161
+ port_part = (@uri.port && @uri.port != @uri.default_port) ? ":#{@uri.port}" : ""
162
+ "#{@uri.scheme}://#{@uri.host}#{port_part}"
163
+ end
164
+
165
+ def username
166
+ @uri.user.to_s
167
+ end
168
+
169
+ def username=(value)
170
+ @uri.user = value.to_s.empty? ? nil : value.to_s
171
+ end
172
+
173
+ def password
174
+ @uri.password.to_s
175
+ end
176
+
177
+ def password=(value)
178
+ @uri.password = value.to_s.empty? ? nil : value.to_s
179
+ end
180
+
181
+ def to_s
182
+ href
183
+ end
184
+
185
+ def to_json(*_args)
186
+ # match JSON.stringify(url) -> "\"<href>\""
187
+ href.inspect
188
+ end
189
+
190
+ def __js_get__(key)
191
+ case key
192
+ when "href"
193
+ href
194
+ when "protocol"
195
+ protocol
196
+ when "host"
197
+ host
198
+ when "hostname"
199
+ hostname
200
+ when "port"
201
+ port
202
+ when "pathname"
203
+ pathname
204
+ when "search"
205
+ search
206
+ when "hash"
207
+ hash
208
+ when "origin"
209
+ origin
210
+ when "username"
211
+ username
212
+ when "password"
213
+ password
214
+ when "searchParams"
215
+ @search_params
216
+ end
217
+ end
218
+
219
+ def __js_set__(key, value)
220
+ case key
221
+ when "href"
222
+ self.href = value
223
+ when "protocol"
224
+ self.protocol = value
225
+ when "host"
226
+ self.host = value
227
+ when "hostname"
228
+ self.hostname = value
229
+ when "port"
230
+ self.port = value
231
+ when "pathname"
232
+ self.pathname = value
233
+ when "search"
234
+ self.search = value
235
+ when "hash"
236
+ self.hash = value
237
+ when "username"
238
+ self.username = value
239
+ when "password"
240
+ self.password = value
241
+ end
242
+
243
+ nil
244
+ end
245
+
246
+ def __js_call__(method, _args)
247
+ case method
248
+ when "toString", "toJSON"
249
+ href
250
+ end
251
+ end
252
+
253
+ # Called by URLSearchParams when it mutates; we need to keep the
254
+ # underlying URI's query string in sync so subsequent `href` is
255
+ # accurate.
256
+ def __notify_params_changed__
257
+ sync_uri_query
258
+ end
259
+
260
+ private
261
+
262
+ def parse_with_base(input, base)
263
+ str = input.to_s
264
+ uri = nil
265
+ if base
266
+ base_uri = base.is_a?(URL) ? URI.parse(base.href) : URI.parse(base.to_s)
267
+ uri = URI.join(base_uri, str)
268
+ else
269
+ uri = URI.parse(str)
270
+ raise DOMException::SyntaxError, "Invalid URL: #{str}" unless uri.scheme
271
+ end
272
+
273
+ uri
274
+ rescue URI::InvalidURIError => e
275
+ raise DOMException::SyntaxError, "Invalid URL: #{e.message}"
276
+ end
277
+
278
+ def build_href
279
+ out = +""
280
+ out << "#{@uri.scheme}:" if @uri.scheme
281
+ if @uri.host
282
+ out << "//"
283
+ if @uri.user
284
+ out << @uri.user
285
+ out << ":#{@uri.password}" if @uri.password
286
+ out << "@"
287
+ end
288
+
289
+ out << @uri.host
290
+ out << ":#{@uri.port}" if @uri.port && @uri.port != @uri.default_port
291
+ end
292
+
293
+ out << @uri.path.to_s
294
+ out << search
295
+ out << hash
296
+ out
297
+ end
298
+
299
+ def sync_uri_query
300
+ q = @search_params.to_s
301
+ @uri.query = q.empty? ? nil : q
302
+ end
303
+ end
304
+
305
+ # `URLSearchParams` — query-string manipulation. Constructed from a
306
+ # raw string (`"a=1&b=2"`), an array of `[k, v]` pairs, or a Hash.
307
+ # Order is preserved. Values are stringified per spec.
308
+ class URLSearchParams
309
+ include Enumerable
310
+
311
+ def initialize(input = "", owner: nil)
312
+ @owner = owner
313
+ @pairs = parse(input)
314
+ end
315
+
316
+ def get(name)
317
+ pair = @pairs.find { |k, _| k == name.to_s }
318
+ pair && pair[1]
319
+ end
320
+
321
+ def get_all(name)
322
+ @pairs.select { |k, _| k == name.to_s }.map { |_, v| v }
323
+ end
324
+
325
+ alias getAll get_all
326
+
327
+ def has(name)
328
+ @pairs.any? { |k, _| k == name.to_s }
329
+ end
330
+
331
+ alias has? has
332
+
333
+ def set(name, value)
334
+ key = name.to_s
335
+ first_done = false
336
+ @pairs = @pairs.reject do |k, _|
337
+ next false unless k == key
338
+
339
+ if first_done
340
+ true
341
+ else
342
+ first_done = true
343
+ false
344
+ end
345
+ end
346
+
347
+ @pairs.map! { |pair| pair[0] == key ? [key, value.to_s] : pair }
348
+ @pairs << [key, value.to_s] unless first_done
349
+ notify
350
+ nil
351
+ end
352
+
353
+ def append(name, value)
354
+ @pairs << [name.to_s, value.to_s]
355
+ notify
356
+ nil
357
+ end
358
+
359
+ def delete(name, value = nil)
360
+ key = name.to_s
361
+ if value.nil?
362
+ @pairs.reject! { |k, _| k == key }
363
+ else
364
+ v = value.to_s
365
+ @pairs.reject! { |k, vv| k == key && vv == v }
366
+ end
367
+
368
+ notify
369
+ nil
370
+ end
371
+
372
+ def sort
373
+ @pairs.sort_by! { |k, _| k }
374
+ notify
375
+ nil
376
+ end
377
+
378
+ def size
379
+ @pairs.length
380
+ end
381
+
382
+ alias length size
383
+
384
+ def each(&block)
385
+ @pairs.each(&block)
386
+ end
387
+
388
+ def keys
389
+ @pairs.map { |k, _| k }
390
+ end
391
+
392
+ def values
393
+ @pairs.map { |_, v| v }
394
+ end
395
+
396
+ def entries
397
+ @pairs.dup
398
+ end
399
+
400
+ def for_each(&block)
401
+ @pairs.each { |k, v| block.call(v, k, self) }
402
+ nil
403
+ end
404
+
405
+ alias forEach for_each
406
+
407
+ def to_s
408
+ @pairs.map { |k, v| "#{encode(k)}=#{encode(v)}" }.join("&")
409
+ end
410
+
411
+ def __replace__(query_string)
412
+ @pairs = parse(query_string)
413
+ nil
414
+ end
415
+
416
+ def __js_get__(key)
417
+ case key
418
+ when "size", "length"
419
+ size
420
+ end
421
+ end
422
+
423
+ def __js_call__(method, args)
424
+ case method
425
+ when "get"
426
+ get(args[0])
427
+ when "getAll"
428
+ get_all(args[0])
429
+ when "has"
430
+ has(args[0])
431
+ when "set"
432
+ set(args[0], args[1])
433
+ when "append"
434
+ append(args[0], args[1])
435
+ when "delete"
436
+ delete(args[0], args[1])
437
+ when "sort"
438
+ sort
439
+ when "toString"
440
+ to_s
441
+ when "forEach"
442
+ for_each(&args[0])
443
+ when "keys"
444
+ keys
445
+ when "values"
446
+ values
447
+ when "entries"
448
+ entries
449
+ end
450
+ end
451
+
452
+ private
453
+
454
+ def parse(input)
455
+ case input
456
+ when Array
457
+ input.map { |k, v| [k.to_s, v.to_s] }
458
+ when Hash
459
+ input.map { |k, v| [k.to_s, v.to_s] }
460
+ else
461
+ s = input.to_s.sub(/^\?/, "")
462
+ return [] if s.empty?
463
+
464
+ s.split("&").map do |pair|
465
+ k, v = pair.split("=", 2)
466
+ [CGI.unescape(k.to_s), CGI.unescape(v.to_s)]
467
+ end
468
+ end
469
+ end
470
+
471
+ def encode(str)
472
+ CGI.escape(str.to_s)
473
+ end
474
+
475
+ def notify
476
+ @owner&.__notify_params_changed__
477
+ end
478
+ end
479
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ VERSION = "0.5.0"
5
+ end
@@ -0,0 +1,209 @@
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
+ @local_storage = Storage.new
50
+ @session_storage = Storage.new
51
+ @location = Location.new(self)
52
+ @history = History.new(self, @location)
53
+ @url_ctor = Bridge::Constructor.new { |args| Url.new(args[0], args[1]) }
54
+ @url_ctor.define_class_method("createObjectURL") { |args| URL.create_object_url(args[0]) }
55
+ @url_ctor.define_class_method("revokeObjectURL") { |args| URL.revoke_object_url(args[0]) }
56
+ # `JS.global[:__some_key__] = ...` from user code lands here.
57
+ # Test code uses this for stub installation (e.g. a custom
58
+ # `__fetch_stub__`); production code stays on the typed
59
+ # accessors above. We keep it last in the read fallback to
60
+ # avoid shadowing intentional getters.
61
+ @globals = {}
62
+ @document = Document.new(host, nokogiri_doc: nokogiri_doc)
63
+ @document.default_view = self
64
+ @custom_elements = CustomElementRegistry.new(self)
65
+ @navigator = Navigator.new(self)
66
+ end
67
+
68
+ # Bridge protocol: respond to a JS-style property read by name.
69
+ # Returns either a Ruby primitive (Integer / String / true / false /
70
+ # nil), a Hash/Array (for JS object/array literals), or a Dom::*
71
+ # instance for live DOM/BOM objects.
72
+ #
73
+ # Anything outside the surface we've explicitly polyfilled returns
74
+ # nil (= JS undefined). Spec failures here are the signal to widen
75
+ # the surface in a future session.
76
+ def __js_get__(key)
77
+ case key
78
+ when "document"
79
+ @document
80
+ when "Event"
81
+ @event_ctor
82
+ when "CustomEvent"
83
+ @custom_event_ctor
84
+ when "MouseEvent"
85
+ @mouse_event_ctor
86
+ when "KeyboardEvent"
87
+ @keyboard_event_ctor
88
+ when "EventTarget"
89
+ @event_target_ctor
90
+ when "Error"
91
+ @error_ctor
92
+ when "Promise"
93
+ @promise_ctor
94
+ when "MutationObserver"
95
+ @mutation_observer_ctor
96
+ when "AbortController"
97
+ @abort_controller_ctor
98
+ when "Blob"
99
+ @blob_ctor
100
+ when "File"
101
+ @file_ctor
102
+ when "FileList"
103
+ @file_list_ctor
104
+ when "DataTransfer"
105
+ @data_transfer_ctor
106
+ when "DragEvent"
107
+ @drag_event_ctor
108
+ # handled by Symbol sentinel
109
+ when "console"
110
+ :console
111
+ # likewise
112
+ when "Object"
113
+ :object_ctor
114
+ when "Array"
115
+ :array_ctor
116
+ when "JSON"
117
+ :json_ctor
118
+ when "performance"
119
+ {"now" => @scheduler.now_ms.to_f}
120
+ when "localStorage"
121
+ @local_storage
122
+ when "sessionStorage"
123
+ @session_storage
124
+ when "location"
125
+ @location
126
+ when "history"
127
+ @history
128
+ when "URL"
129
+ @url_ctor
130
+ when "fetch"
131
+ FetchFn.new(self)
132
+ when "customElements"
133
+ @custom_elements
134
+ when "navigator"
135
+ @navigator
136
+ else
137
+ @globals[key]
138
+ end
139
+ end
140
+
141
+ def __js_set__(key, value)
142
+ # Stash arbitrary keys for later reads (e.g.
143
+ # `JS.global[:__fetchy_stub__] = map`).
144
+ @globals[key] = value
145
+ # The Fetchy spec's `install_fetch_stub` resets `__fetch_count__`
146
+ # to 0 inside its JS installer (`globalThis.__fetch_count__ = 0;
147
+ # globalThis.fetch = ...`). Our polyfill ignores raw JS, so we
148
+ # piggy-back on the stub assignment to perform the same reset
149
+ # — without it the count accumulates across tests in one VM run.
150
+ @globals["__fetch_count__"] = 0 if %w[__fetchy_stub__ __resource_fetch_stub__ __inject_fetch_stub__].include?(key)
151
+ nil
152
+ end
153
+
154
+ def __js_call__(method, args)
155
+ case method
156
+ when "fetch"
157
+ FetchFn.new(self).__js_call__("call", args)
158
+ when "encodeURIComponent"
159
+ # JS spec encoding: percent-encode anything except
160
+ # `A-Za-z0-9 - _ . ! ~ * ' ( )`. Ruby's `CGI.escape` uses
161
+ # `+` for space; ERB::Util.url_encode matches JS behavior.
162
+ ERB::Util.url_encode(args[0].to_s)
163
+ when "decodeURIComponent"
164
+ CGI.unescape(args[0].to_s)
165
+ when "addEventListener"
166
+ add_event_listener(args[0], args[1], args[2])
167
+ when "removeEventListener"
168
+ remove_event_listener(args[0], args[1])
169
+ when "dispatchEvent"
170
+ dispatch_event(args[0])
171
+ when "setTimeout"
172
+ @scheduler.set_timeout(args[0], args[1] || 0)
173
+ when "clearTimeout"
174
+ @scheduler.clear_timeout(args[0])
175
+ when "setInterval"
176
+ @scheduler.set_interval(args[0], args[1] || 0)
177
+ when "clearInterval"
178
+ @scheduler.clear_interval(args[0])
179
+ when "requestAnimationFrame"
180
+ @scheduler.request_animation_frame(args[0])
181
+ when "cancelAnimationFrame"
182
+ @scheduler.cancel_animation_frame(args[0])
183
+ when "queueMicrotask"
184
+ @scheduler.queue_microtask(args[0])
185
+ else
186
+ # Additional window-level methods (fetch, location, history,
187
+ # Promise, MutationObserver, etc.) arrive in later sessions.
188
+ nil
189
+ end
190
+ end
191
+
192
+ def __event_parent__
193
+ nil
194
+ end
195
+
196
+ # Called by History#go and Location.href= to fire popstate /
197
+ # hashchange events. Listeners registered on the Window via
198
+ # `addEventListener("popstate"|"hashchange", cb)` receive them.
199
+ def fire_popstate(state)
200
+ event = CustomEvent.new("popstate", "detail" => state)
201
+ dispatch_event(event)
202
+ end
203
+
204
+ def fire_hashchange(old_hash, new_hash)
205
+ event = CustomEvent.new("hashchange", "detail" => {"oldURL" => old_hash, "newURL" => new_hash})
206
+ dispatch_event(event)
207
+ end
208
+ end
209
+ end