Class: Dommy::Window

Inherits:
Object
  • Object
show all
Includes:
EventTarget
Defined in:
lib/dommy/window.rb

Overview

The browser global. ‘JS.global` from inside wasm resolves to this. Property access (`JS.global`, `JS.global`) is routed through `#js_get`. Method calls (`JS.global.call(:foo)`) are routed through `#js_call`.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EventTarget

#__deliver_event__, #add_event_listener, #dispatch_event, #invoke_listener, #remove_event_listener

Constructor Details

#initialize(host = nil, nokogiri_doc: nil) ⇒ Window

Returns a new instance of Window.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/dommy/window.rb', line 26

def initialize(host = nil, nokogiri_doc: nil)
  @host = host
  @scheduler = Scheduler.new
  @event_ctor = Bridge::Constructor.new { |args| Event.new(args[0], args[1]) }
  @custom_event_ctor = Bridge::Constructor.new { |args| CustomEvent.new(args[0], args[1]) }
  @mouse_event_ctor = Bridge::Constructor.new { |args| MouseEvent.new(args[0], args[1]) }
  @keyboard_event_ctor = Bridge::Constructor.new { |args| KeyboardEvent.new(args[0], args[1]) }
  @event_target_ctor = Bridge::Constructor.new { |_args| StandaloneEventTarget.new }
  @error_ctor = Bridge::Constructor.new { |args| ErrorValue.new(args[0]) }
  @promise_ctor = Bridge::PromiseConstructor.new(self)
  @mutation_observer_ctor = Bridge::Constructor.new { |args| MutationObserver.new(self, args[0]) }
  @abort_controller_ctor = Bridge::Constructor.new { |_args| AbortController.new }
  @blob_ctor = Bridge::Constructor.new { |args| Blob.new(args[0] || [], args[1] || {}) }
  @file_ctor = Bridge::Constructor.new { |args| File.new(args[0] || [], args[1].to_s, args[2] || {}) }
  @file_list_ctor = Bridge::Constructor.new { |args| FileList.new(args[0] || []) }
  @data_transfer_ctor = Bridge::Constructor.new { |args|
    opts = args[0] || {}
    DataTransfer.new(
      files: opts["files"] || opts[:files] || [],
      data: opts["data"] || opts[:data] || {}
    )
  }
  @drag_event_ctor = Bridge::Constructor.new { |args| DragEvent.new(args[0], args[1]) }
  @input_event_ctor = Bridge::Constructor.new { |args| InputEvent.new(args[0], args[1]) }
  @pointer_event_ctor = Bridge::Constructor.new { |args| PointerEvent.new(args[0], args[1]) }
  @progress_event_ctor = Bridge::Constructor.new { |args| ProgressEvent.new(args[0], args[1]) }
  @touch_ctor = Bridge::Constructor.new { |args| Touch.new(args[0] || {}) }
  @touch_event_ctor = Bridge::Constructor.new { |args| TouchEvent.new(args[0], args[1]) }
  @clipboard_event_ctor = Bridge::Constructor.new { |args| ClipboardEvent.new(args[0], args[1]) }
  @composition_event_ctor = Bridge::Constructor.new { |args| CompositionEvent.new(args[0], args[1]) }
  @wheel_event_ctor = Bridge::Constructor.new { |args| WheelEvent.new(args[0], args[1]) }
  @focus_event_ctor = Bridge::Constructor.new { |args| FocusEvent.new(args[0], args[1]) }
  @before_unload_event_ctor = Bridge::Constructor.new { |args|
    BeforeUnloadEvent.new(args[0] || "beforeunload", args[1])
  }
  win_ref = self
  @animation_ctor = Bridge::Constructor.new { |args| Animation.new(args[0], args[1], window: win_ref) }
  @keyframe_effect_ctor = Bridge::Constructor.new { |args| KeyframeEffect.new(args[0], args[1] || [], args[2]) }
  @crypto = Crypto.new(self)
  @text_encoder_ctor = Bridge::Constructor.new { |_args| TextEncoder.new }
  @text_decoder_ctor = Bridge::Constructor.new { |args| TextDecoder.new(args[0] || "utf-8", args[1]) }
  @intersection_observer_ctor = Bridge::Constructor.new { |args| IntersectionObserver.new(args[0], args[1]) }
  @resize_observer_ctor = Bridge::Constructor.new { |args| ResizeObserver.new(args[0]) }
  @performance_observer_ctor = Bridge::Constructor.new { |args| PerformanceObserver.new(args[0]) }
  @request_ctor = Bridge::Constructor.new { |args| Request.new(args[0], args[1]) }
  xhr_win_ref = self
  @xhr_ctor = Bridge::Constructor.new { |_args| XMLHttpRequest.new(xhr_win_ref) }
  @file_reader_ctor = Bridge::Constructor.new { |_args| FileReader.new(xhr_win_ref) }
  @message_channel_ctor = Bridge::Constructor.new { |_args| MessageChannel.new(xhr_win_ref) }
  @broadcast_channel_ctor = Bridge::Constructor.new { |args| BroadcastChannel.new(xhr_win_ref, args[0]) }
  @web_socket_ctor = Bridge::Constructor.new { |args| WebSocket.new(xhr_win_ref, args[0], args[1]) }
  @event_source_ctor = Bridge::Constructor.new { |args| EventSource.new(xhr_win_ref, args[0], args[1]) }
  @notification_ctor = Bridge::Constructor.new { |args| Notification.new(xhr_win_ref, args[0], args[1]) }
  @notification_ctor.define_class_method("requestPermission") do |args|
    Notification.request_permission(xhr_win_ref, args[0])
  end

  @worker_ctor = Bridge::Constructor.new { |args| Worker.new(xhr_win_ref, args[0], args[1]) }
  @readable_stream_ctor = Bridge::Constructor.new { |args| ReadableStream.new(xhr_win_ref, args[0]) }
  @writable_stream_ctor = Bridge::Constructor.new { |args| WritableStream.new(xhr_win_ref, args[0]) }
  @transform_stream_ctor = Bridge::Constructor.new { |args| TransformStream.new(xhr_win_ref, args[0]) }
  @text_encoder_stream_ctor = Bridge::Constructor.new { |_args| TextEncoderStream.new(xhr_win_ref) }
  @text_decoder_stream_ctor = Bridge::Constructor.new { |args|
    TextDecoderStream.new(xhr_win_ref, args[0] || "utf-8", args[1])
  }
  @compression_stream_ctor = Bridge::Constructor.new { |args| CompressionStream.new(xhr_win_ref, args[0]) }
  @decompression_stream_ctor = Bridge::Constructor.new { |args| DecompressionStream.new(xhr_win_ref, args[0]) }
  @url_pattern_ctor = Bridge::Constructor.new { |args| URLPattern.new(args[0], args[1]) }
  @cookie_store = CookieStore.new(xhr_win_ref)

  @range_ctor = Bridge::Constructor.new { |_args| Range.new(@document) }
  @local_storage = Storage.new
  @session_storage = Storage.new
  @location = Location.new(self)
  @history = History.new(self, @location)
  @url_ctor = Bridge::Constructor.new { |args| URL.new(args[0], args[1]) }
  @url_ctor.define_class_method("createObjectURL") { |args| URL.create_object_url(args[0]) }
  @url_ctor.define_class_method("revokeObjectURL") { |args| URL.revoke_object_url(args[0]) }
  # `JS.global[:__some_key__] = ...` from user code lands here.
  # Test code uses this for stub installation (e.g. a custom
  # `__fetch_stub__`); production code stays on the typed
  # accessors above. We keep it last in the read fallback to
  # avoid shadowing intentional getters.
  @globals = {}
  @document = Document.new(host, nokogiri_doc: nokogiri_doc)
  @document.default_view = self
  @custom_elements = CustomElementRegistry.new(self)
  @navigator = Navigator.new(self)
end

Instance Attribute Details

#custom_elementsObject (readonly)

Returns the value of attribute custom_elements.



24
25
26
# File 'lib/dommy/window.rb', line 24

def custom_elements
  @custom_elements
end

#documentObject (readonly)

Returns the value of attribute document.



24
25
26
# File 'lib/dommy/window.rb', line 24

def document
  @document
end

#globalsObject (readonly)

Returns the value of attribute globals.



24
25
26
# File 'lib/dommy/window.rb', line 24

def globals
  @globals
end

#locationObject (readonly)

Returns the value of attribute location.



24
25
26
# File 'lib/dommy/window.rb', line 24

def location
  @location
end

Returns the value of attribute navigator.



24
25
26
# File 'lib/dommy/window.rb', line 24

def navigator
  @navigator
end

#schedulerObject (readonly)

Returns the value of attribute scheduler.



24
25
26
# File 'lib/dommy/window.rb', line 24

def scheduler
  @scheduler
end

Instance Method Details

#__event_parent__Object



339
340
341
# File 'lib/dommy/window.rb', line 339

def __event_parent__
  nil
end

#__js_call__(method, args) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/dommy/window.rb', line 276

def __js_call__(method, args)
  case method
  when "fetch"
    FetchFn.new(self).__js_call__("call", args)
  when "encodeURIComponent"
    # JS spec encoding: percent-encode anything except
    # `A-Za-z0-9 - _ . ! ~ * ' ( )`. Ruby's `CGI.escape` uses
    # `+` for space; ERB::Util.url_encode matches JS behavior.
    ERB::Util.url_encode(args[0].to_s)
  when "decodeURIComponent"
    CGI.unescape(args[0].to_s)
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1])
  when "dispatchEvent"
    dispatch_event(args[0])
  when "setTimeout"
    @scheduler.set_timeout(args[0], args[1] || 0)
  when "clearTimeout"
    @scheduler.clear_timeout(args[0])
  when "setInterval"
    @scheduler.set_interval(args[0], args[1] || 0)
  when "clearInterval"
    @scheduler.clear_interval(args[0])
  when "requestAnimationFrame"
    @scheduler.request_animation_frame(args[0])
  when "cancelAnimationFrame"
    @scheduler.cancel_animation_frame(args[0])
  when "queueMicrotask"
    @scheduler.queue_microtask(args[0])
  when "requestIdleCallback"
    # WHATWG `requestIdleCallback` — no real idle period in
    # dommy, so we model it as a deferred setTimeout. The
    # callback receives an `IdleDeadline`-shaped Hash.
    @scheduler.set_timeout(
      proc {
        args[0].respond_to?(:__js_call__) ? args[0].__js_call__(
          "call",
          [{"timeRemaining" => 50.0, "didTimeout" => false}]
        ) : args[0].call({"timeRemaining" => 50.0, "didTimeout" => false})
      },
      (args[1].is_a?(Hash) && args[1]["timeout"]) || 0
    )
  when "cancelIdleCallback"
    @scheduler.clear_timeout(args[0])
  when "structuredClone"
    Dommy.structured_clone(args[0])
  when "matchMedia"
    MediaQueryList.new(self, args[0].to_s)
  when "getComputedStyle"
    # No CSS engine — return the element's inline style. That
    # covers `getComputedStyle(el).getPropertyValue("color")` for
    # values the test set inline via `el.style.color = "..."`.
    target = args[0]
    target.respond_to?(:style) ? target.style : nil
  else
    # Additional window-level methods (fetch, location, history,
    # Promise, MutationObserver, etc.) arrive in later sessions.
    nil
  end
end

#__js_get__(key) ⇒ Object

Bridge protocol: respond to a JS-style property read by name. Returns either a Ruby primitive (Integer / String / true / false / nil), a Hash/Array (for JS object/array literals), or a Dom::* instance for live DOM/BOM objects.

Anything outside the surface we’ve explicitly polyfilled returns nil (= JS undefined). Spec failures here are the signal to widen the surface in a future session.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/dommy/window.rb', line 124

def __js_get__(key)
  case key
  when "document"
    @document
  when "Event"
    @event_ctor
  when "CustomEvent"
    @custom_event_ctor
  when "MouseEvent"
    @mouse_event_ctor
  when "KeyboardEvent"
    @keyboard_event_ctor
  when "EventTarget"
    @event_target_ctor
  when "Error"
    @error_ctor
  when "Promise"
    @promise_ctor
  when "MutationObserver"
    @mutation_observer_ctor
  when "AbortController"
    @abort_controller_ctor
  when "Blob"
    @blob_ctor
  when "File"
    @file_ctor
  when "FileList"
    @file_list_ctor
  when "DataTransfer"
    @data_transfer_ctor
  when "DragEvent"
    @drag_event_ctor
  when "InputEvent"
    @input_event_ctor
  when "PointerEvent"
    @pointer_event_ctor
  when "ProgressEvent"
    @progress_event_ctor
  when "Touch"
    @touch_ctor
  when "TouchEvent"
    @touch_event_ctor
  when "ClipboardEvent"
    @clipboard_event_ctor
  when "CompositionEvent"
    @composition_event_ctor
  when "WheelEvent"
    @wheel_event_ctor
  when "FocusEvent"
    @focus_event_ctor
  when "BeforeUnloadEvent"
    @before_unload_event_ctor
  when "Animation"
    @animation_ctor
  when "KeyframeEffect"
    @keyframe_effect_ctor
  when "crypto"
    @crypto
  when "TextEncoder"
    @text_encoder_ctor
  when "TextDecoder"
    @text_decoder_ctor
  when "IntersectionObserver"
    @intersection_observer_ctor
  when "ResizeObserver"
    @resize_observer_ctor
  when "PerformanceObserver"
    @performance_observer_ctor
  when "Request"
    @request_ctor
  when "XMLHttpRequest"
    @xhr_ctor
  when "FileReader"
    @file_reader_ctor
  when "MessageChannel"
    @message_channel_ctor
  when "BroadcastChannel"
    @broadcast_channel_ctor
  when "WebSocket"
    @web_socket_ctor
  when "EventSource"
    @event_source_ctor
  when "Notification"
    @notification_ctor
  when "Worker"
    @worker_ctor
  when "ReadableStream"
    @readable_stream_ctor
  when "WritableStream"
    @writable_stream_ctor
  when "TransformStream"
    @transform_stream_ctor
  when "TextEncoderStream"
    @text_encoder_stream_ctor
  when "TextDecoderStream"
    @text_decoder_stream_ctor
  when "CompressionStream"
    @compression_stream_ctor
  when "DecompressionStream"
    @decompression_stream_ctor
  when "URLPattern"
    @url_pattern_ctor
  when "cookieStore"
    @cookie_store
  when "Range"
    @range_ctor
    # handled by Symbol sentinel
  when "console"
    :console
    # likewise
  when "Object"
    :object_ctor
  when "Array"
    :array_ctor
  when "JSON"
    :json_ctor
  when "performance"
    @performance ||= Performance.new(self)
  when "localStorage"
    @local_storage
  when "sessionStorage"
    @session_storage
  when "location"
    @location
  when "history"
    @history
  when "URL"
    @url_ctor
  when "fetch"
    FetchFn.new(self)
  when "customElements"
    @custom_elements
  when "navigator"
    @navigator
  else
    @globals[key]
  end
end

#__js_set__(key, value) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/dommy/window.rb', line 263

def __js_set__(key, value)
  # Stash arbitrary keys for later reads (e.g.
  # `JS.global[:__fetchy_stub__] = map`).
  @globals[key] = value
  # The Fetchy spec's `install_fetch_stub` resets `__fetch_count__`
  # to 0 inside its JS installer (`globalThis.__fetch_count__ = 0;
  # globalThis.fetch = ...`). Our polyfill ignores raw JS, so we
  # piggy-back on the stub assignment to perform the same reset
  # — without it the count accumulates across tests in one VM run.
  @globals["__fetch_count__"] = 0 if %w[__fetchy_stub__ __resource_fetch_stub__ __inject_fetch_stub__].include?(key)
  nil
end

#fire_hashchange(old_hash, new_hash) ⇒ Object



351
352
353
354
# File 'lib/dommy/window.rb', line 351

def fire_hashchange(old_hash, new_hash)
  event = CustomEvent.new("hashchange", "detail" => {"oldURL" => old_hash, "newURL" => new_hash})
  dispatch_event(event)
end

#fire_popstate(state) ⇒ Object

Called by History#go and Location.href= to fire popstate / hashchange events. Listeners registered on the Window via ‘addEventListener(“popstate”|“hashchange”, cb)` receive them.



346
347
348
349
# File 'lib/dommy/window.rb', line 346

def fire_popstate(state)
  event = CustomEvent.new("popstate", "detail" => state)
  dispatch_event(event)
end