dommy 0.8.0 → 0.9.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 +3 -3
- data/lib/dommy/animation.rb +4 -0
- data/lib/dommy/attr.rb +11 -5
- data/lib/dommy/backend/makiri_adapter.rb +330 -0
- data/lib/dommy/backend.rb +114 -33
- data/lib/dommy/blob.rb +2 -0
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +217 -0
- data/lib/dommy/compression_streams.rb +4 -0
- data/lib/dommy/crypto.rb +4 -0
- data/lib/dommy/css.rb +487 -50
- data/lib/dommy/custom_elements.rb +2 -2
- data/lib/dommy/data_transfer.rb +2 -0
- data/lib/dommy/data_uri.rb +35 -0
- data/lib/dommy/deferred_response.rb +59 -0
- data/lib/dommy/document.rb +386 -228
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +502 -155
- data/lib/dommy/event.rb +240 -9
- data/lib/dommy/fetch.rb +152 -34
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +2 -0
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +5 -6
- data/lib/dommy/html_elements.rb +304 -27
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +244 -0
- data/lib/dommy/interaction/event_synthesis.rb +56 -0
- data/lib/dommy/interaction/field_interactor.rb +117 -0
- data/lib/dommy/interaction/form_submission.rb +268 -0
- data/lib/dommy/interaction/locator.rb +158 -0
- data/lib/dommy/interaction/role_query.rb +58 -0
- data/lib/dommy/interaction.rb +32 -0
- data/lib/dommy/internal/accessibility_tree.rb +215 -0
- data/lib/dommy/internal/accessible_description.rb +38 -0
- data/lib/dommy/internal/accessible_name.rb +301 -0
- data/lib/dommy/internal/aria_role.rb +252 -0
- data/lib/dommy/internal/aria_snapshot.rb +64 -0
- data/lib/dommy/internal/aria_state.rb +151 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +430 -0
- data/lib/dommy/internal/css/color.rb +381 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
- data/lib/dommy/internal/css/counters.rb +227 -0
- data/lib/dommy/internal/css/custom_properties.rb +183 -0
- data/lib/dommy/internal/css/media_query.rb +302 -0
- data/lib/dommy/internal/css/parser.rb +265 -0
- data/lib/dommy/internal/css/property_registry.rb +512 -0
- data/lib/dommy/internal/css/rule_index.rb +494 -0
- data/lib/dommy/internal/css/supports.rb +158 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
- data/lib/dommy/internal/css_rule_text.rb +160 -0
- data/lib/dommy/internal/dom_matching.rb +80 -9
- data/lib/dommy/internal/element_matching.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +33 -0
- data/lib/dommy/internal/mutation_coordinator.rb +95 -4
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
- data/lib/dommy/internal/parent_node.rb +82 -5
- data/lib/dommy/internal/selector_ast.rb +124 -0
- data/lib/dommy/internal/selector_index.rb +146 -0
- data/lib/dommy/internal/selector_matcher.rb +756 -0
- data/lib/dommy/internal/selector_parser.rb +283 -131
- data/lib/dommy/internal/shadow_root_registry.rb +9 -2
- data/lib/dommy/internal/template_content_registry.rb +26 -18
- data/lib/dommy/internal/xml_serialization.rb +344 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/js/bridge_conformance.rb +80 -0
- data/lib/dommy/js/constructor_resolver.rb +44 -0
- data/lib/dommy/js/custom_element_bridge.rb +90 -0
- data/lib/dommy/js/dom_interfaces.rb +162 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +517 -0
- data/lib/dommy/js/host_runtime.js +1495 -0
- data/lib/dommy/js/import_map.rb +58 -0
- data/lib/dommy/js/marshaller.rb +240 -0
- data/lib/dommy/js/module_loader.rb +99 -0
- data/lib/dommy/js/observable_runtime.js +742 -0
- data/lib/dommy/js/runtime.rb +115 -0
- data/lib/dommy/js/script_boot.rb +221 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +50 -14
- data/lib/dommy/message_channel.rb +22 -6
- data/lib/dommy/minitest/assertions.rb +27 -0
- data/lib/dommy/mutation_observer.rb +89 -4
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +24 -14
- data/lib/dommy/notification.rb +2 -0
- data/lib/dommy/parser.rb +1 -1
- data/lib/dommy/performance.rb +21 -1
- data/lib/dommy/promise.rb +94 -10
- data/lib/dommy/range.rb +173 -31
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +149 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +76 -13
- data/lib/dommy/storage.rb +2 -1
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +33 -10
- data/lib/dommy/url.rb +13 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +199 -11
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +47 -6
- data/lib/dommy.rb +36 -1
- metadata +96 -10
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
data/lib/dommy/version.rb
CHANGED
data/lib/dommy/window.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require_relative "internal/css/cascade"
|
|
6
|
+
require_relative "internal/css/media_query"
|
|
7
|
+
|
|
3
8
|
# Dommy — a happy-dom-style DOM polyfill in pure Ruby. Backbone is
|
|
4
9
|
# Nokogiri::HTML5 plus a small scheduler/event-loop layer.
|
|
5
10
|
#
|
|
@@ -20,7 +25,14 @@ module Dommy
|
|
|
20
25
|
|
|
21
26
|
attr_reader :document, :scheduler, :location, :globals, :custom_elements, :navigator
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
# Opt into best-effort geometry: when true, getBoundingClientRect / client* /
|
|
29
|
+
# offset* return non-zero estimates from a cheap pseudo-layout (viewport width
|
|
30
|
+
# + text content) instead of all-zero. Off by default so the no-layout
|
|
31
|
+
# contract (and the tests asserting 0) is unchanged; a browser front end
|
|
32
|
+
# (dommynx) turns it on so sites that bail on all-zero rects can proceed.
|
|
33
|
+
attr_accessor :approximate_layout
|
|
34
|
+
|
|
35
|
+
def initialize(host = nil, backend_doc: nil)
|
|
24
36
|
@host = host
|
|
25
37
|
@scheduler = Scheduler.new
|
|
26
38
|
@crypto = Crypto.new(self)
|
|
@@ -35,8 +47,15 @@ module Dommy
|
|
|
35
47
|
# production code stays on the typed accessors. Kept last in the read
|
|
36
48
|
# fallback so it can't shadow intentional getters.
|
|
37
49
|
@globals = {}
|
|
38
|
-
@document = Document.new(host,
|
|
50
|
+
@document = Document.new(host, backend_doc: backend_doc)
|
|
39
51
|
@document.default_view = self
|
|
52
|
+
# Per the HTML parsing algorithm, a <template>'s contents are parsed into a
|
|
53
|
+
# separate "template contents" DocumentFragment, not as children of the
|
|
54
|
+
# element. Backends (libxml2) leave them as direct children, so migrate
|
|
55
|
+
# eagerly at page-load time — before any framework walks the tree. Without
|
|
56
|
+
# this, a tree-walk (Alpine's x-for/x-if scan, etc.) descends into the
|
|
57
|
+
# template's inert content and evaluates directives there out of scope.
|
|
58
|
+
@document.migrate_template_descendants(@document.backend_doc)
|
|
40
59
|
@custom_elements = CustomElementRegistry.new(self)
|
|
41
60
|
@navigator = Navigator.new(self)
|
|
42
61
|
# All JS global constructors (`new Event()`, `new URL()`, ...) live in a
|
|
@@ -95,6 +114,14 @@ module Dommy
|
|
|
95
114
|
@custom_elements
|
|
96
115
|
when "navigator"
|
|
97
116
|
@navigator
|
|
117
|
+
when "screen"
|
|
118
|
+
@screen ||= Screen.new(self)
|
|
119
|
+
when "innerWidth", "outerWidth"
|
|
120
|
+
media_environment.viewport_width
|
|
121
|
+
when "innerHeight", "outerHeight"
|
|
122
|
+
media_environment.viewport_height
|
|
123
|
+
when "devicePixelRatio"
|
|
124
|
+
media_environment.device_pixel_ratio
|
|
98
125
|
when "scrollX", "pageXOffset"
|
|
99
126
|
@scroll_x || 0
|
|
100
127
|
when "scrollY", "pageYOffset"
|
|
@@ -102,8 +129,18 @@ module Dommy
|
|
|
102
129
|
when "scrollMaxX", "scrollMaxY"
|
|
103
130
|
# No real content box to scroll past, so the max offset is 0.
|
|
104
131
|
0
|
|
132
|
+
when "event"
|
|
133
|
+
# The legacy global current-event is *absent* (undefined, not null) when
|
|
134
|
+
# no event is being dispatched, so feature detection like
|
|
135
|
+
# `window.event === undefined` (React's getCurrentEventPriority) takes the
|
|
136
|
+
# not-supported path instead of dereferencing null. An explicitly-set
|
|
137
|
+
# value still wins.
|
|
138
|
+
@globals.key?("event") ? @globals["event"] : Bridge::UNDEFINED
|
|
105
139
|
else
|
|
106
|
-
|
|
140
|
+
# A stashed global wins (even if its value is nil/null); a key never set
|
|
141
|
+
# is genuinely absent → ABSENT so JS sees `undefined` and `"x" in window`
|
|
142
|
+
# is false (feature detection like `isUndefined(window.Vue)` works).
|
|
143
|
+
@globals.key?(key) ? @globals[key] : Bridge::ABSENT
|
|
107
144
|
end
|
|
108
145
|
end
|
|
109
146
|
|
|
@@ -122,10 +159,11 @@ module Dommy
|
|
|
122
159
|
|
|
123
160
|
include Bridge::Methods
|
|
124
161
|
js_methods %w[
|
|
125
|
-
fetch encodeURIComponent decodeURIComponent addEventListener removeEventListener
|
|
162
|
+
fetch encodeURIComponent decodeURIComponent btoa atob addEventListener removeEventListener
|
|
126
163
|
dispatchEvent setTimeout clearTimeout setInterval clearInterval requestAnimationFrame
|
|
127
164
|
cancelAnimationFrame queueMicrotask requestIdleCallback cancelIdleCallback structuredClone
|
|
128
|
-
matchMedia getComputedStyle scroll scrollTo scrollBy
|
|
165
|
+
matchMedia getComputedStyle scroll scrollTo scrollBy resizeTo
|
|
166
|
+
alert confirm prompt open reportError getSelection postMessage
|
|
129
167
|
]
|
|
130
168
|
def __js_call__(method, args)
|
|
131
169
|
case method
|
|
@@ -135,6 +173,10 @@ module Dommy
|
|
|
135
173
|
Internal::GlobalFunctions.encode_uri_component(args[0])
|
|
136
174
|
when "decodeURIComponent"
|
|
137
175
|
Internal::GlobalFunctions.decode_uri_component(args[0])
|
|
176
|
+
when "btoa"
|
|
177
|
+
Internal::GlobalFunctions.btoa(args[0])
|
|
178
|
+
when "atob"
|
|
179
|
+
Internal::GlobalFunctions.atob(args[0])
|
|
138
180
|
when "addEventListener"
|
|
139
181
|
add_event_listener(args[0], args[1], args[2])
|
|
140
182
|
when "removeEventListener"
|
|
@@ -164,15 +206,27 @@ module Dommy
|
|
|
164
206
|
when "matchMedia"
|
|
165
207
|
MediaQueryList.new(self, args[0].to_s)
|
|
166
208
|
when "getComputedStyle"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
target = args[0]
|
|
171
|
-
target.respond_to?(:style) ? target.style : nil
|
|
209
|
+
get_computed_style(args[0], args[1])
|
|
210
|
+
when "resizeTo"
|
|
211
|
+
resize_to(args[0], args[1])
|
|
172
212
|
when "scroll", "scrollTo"
|
|
173
213
|
scroll_to(*args)
|
|
174
214
|
when "scrollBy"
|
|
175
215
|
scroll_by(*args)
|
|
216
|
+
when "alert"
|
|
217
|
+
nil # headless: no dialog (happy-dom semantics)
|
|
218
|
+
when "confirm"
|
|
219
|
+
false # no user -> treated as "Cancel"
|
|
220
|
+
when "prompt"
|
|
221
|
+
nil # no user input
|
|
222
|
+
when "open"
|
|
223
|
+
nil # cannot open a new browsing context headlessly
|
|
224
|
+
when "reportError"
|
|
225
|
+
nil # swallow programmatic error reports (no uncaught surfacing here)
|
|
226
|
+
when "getSelection"
|
|
227
|
+
document&.get_selection
|
|
228
|
+
when "postMessage"
|
|
229
|
+
post_message(args[0])
|
|
176
230
|
else
|
|
177
231
|
# Additional window-level methods (fetch, location, history,
|
|
178
232
|
# Promise, MutationObserver, etc.) arrive in later sessions.
|
|
@@ -199,6 +253,80 @@ module Dommy
|
|
|
199
253
|
dispatch_event(event)
|
|
200
254
|
end
|
|
201
255
|
|
|
256
|
+
# --- Viewport / media environment (cssom-view) ---
|
|
257
|
+
|
|
258
|
+
# The media-feature environment matchMedia and @media evaluate against
|
|
259
|
+
# (viewport 1280x720, light scheme, dpr 1 by default). Mutable; after a
|
|
260
|
+
# direct mutation call __internal_media_environment_changed__ to propagate —
|
|
261
|
+
# resize_to does both for the viewport.
|
|
262
|
+
def media_environment
|
|
263
|
+
@media_environment ||= Internal::CSS::MediaQuery::Environment.default
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def inner_width = media_environment.viewport_width
|
|
267
|
+
def inner_height = media_environment.viewport_height
|
|
268
|
+
|
|
269
|
+
# Resolve a (possibly relative) URL against the document base URL — the
|
|
270
|
+
# API base URL of this window's environment, as fetch/XHR use when
|
|
271
|
+
# constructing a request. Returns the input unchanged if it can't resolve.
|
|
272
|
+
def __internal_resolve_url__(url)
|
|
273
|
+
base = @document&.base_uri
|
|
274
|
+
return url.to_s if base.to_s.empty?
|
|
275
|
+
|
|
276
|
+
URI.join(base.to_s, url.to_s).to_s
|
|
277
|
+
rescue URI::Error
|
|
278
|
+
url.to_s
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# The path (with query) of a URL — lets a stub keyed by a path ("/api")
|
|
282
|
+
# match its resolved absolute form ("http://host/api").
|
|
283
|
+
def __internal_url_path__(url)
|
|
284
|
+
uri = URI.parse(url.to_s)
|
|
285
|
+
uri.query ? "#{uri.path}?#{uri.query}" : uri.path
|
|
286
|
+
rescue URI::Error
|
|
287
|
+
url.to_s
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Resize the virtual viewport: updates the environment, invalidates
|
|
291
|
+
# computed styles (@media), re-evaluates handed-out MediaQueryLists
|
|
292
|
+
# (firing their `change` events), and fires the window `resize` event.
|
|
293
|
+
def resize_to(width, height)
|
|
294
|
+
media_environment.viewport_width = width.to_i
|
|
295
|
+
media_environment.viewport_height = height.to_i
|
|
296
|
+
__internal_media_environment_changed__
|
|
297
|
+
dispatch_event(Event.new("resize"))
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def __internal_media_environment_changed__
|
|
302
|
+
@document&.__internal_bump_style_generation__
|
|
303
|
+
(@media_query_lists || []).each(&:__internal_environment_changed__)
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def __internal_register_media_query_list__(mql)
|
|
308
|
+
(@media_query_lists ||= []) << mql
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# CSSOM getComputedStyle. With the makiri-backed CSS parser available
|
|
313
|
+
# this resolves the full cascade (UA sheet + <style> sheets + style
|
|
314
|
+
# attribute); without it, falls back to the element's inline style (the
|
|
315
|
+
# historical behavior). A pseudo-element argument yields an empty
|
|
316
|
+
# declaration (Dommy renders no ::before/::after boxes).
|
|
317
|
+
def get_computed_style(element, pseudo_element = nil)
|
|
318
|
+
return nil unless element
|
|
319
|
+
|
|
320
|
+
if Internal::CSS::Parser.available?
|
|
321
|
+
pseudo = pseudo_element.to_s
|
|
322
|
+
Internal::CSS::ComputedStyleDeclaration.new(
|
|
323
|
+
element, pseudo_element: pseudo.empty? ? nil : pseudo
|
|
324
|
+
)
|
|
325
|
+
else
|
|
326
|
+
element.respond_to?(:style) ? element.style : nil
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
202
330
|
private
|
|
203
331
|
|
|
204
332
|
# Virtual scroll position. There's no real layout, but tracking a logical
|
|
@@ -216,6 +344,15 @@ module Dommy
|
|
|
216
344
|
update_scroll(x, y)
|
|
217
345
|
end
|
|
218
346
|
|
|
347
|
+
# `window.postMessage`: deliver a structured-cloned `message` to this window's
|
|
348
|
+
# own message handlers from a TASK (the "post message" task source, not a
|
|
349
|
+
# microtask) — so it lands in a later event-loop turn, as the spec requires.
|
|
350
|
+
def post_message(message)
|
|
351
|
+
data = Dommy.structured_clone(message)
|
|
352
|
+
@scheduler.set_timeout(proc { dispatch_event(MessageEvent.new("message", "data" => data)) }, 0)
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
|
|
219
356
|
# Accept either positional `(x, y)` or a `{ left:, top: }` options dict.
|
|
220
357
|
def parse_scroll_args(args, cur_x, cur_y, relative:)
|
|
221
358
|
if args[0].is_a?(Hash)
|
|
@@ -256,6 +393,20 @@ module Dommy
|
|
|
256
393
|
0
|
|
257
394
|
end
|
|
258
395
|
|
|
396
|
+
# WebIDL coercion for the `Text`/`Comment` constructor's `optional DOMString
|
|
397
|
+
# data = ""`: an omitted or undefined argument uses the default (""), but an
|
|
398
|
+
# explicit `null` stringifies to "null" per ToString. (Omitted arrives as an
|
|
399
|
+
# empty args list; explicit JS null arrives as a Ruby nil element.)
|
|
400
|
+
def node_data_arg(args)
|
|
401
|
+
return "" if args.empty?
|
|
402
|
+
|
|
403
|
+
value = args[0]
|
|
404
|
+
return "" if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
|
|
405
|
+
return "null" if value.nil?
|
|
406
|
+
|
|
407
|
+
value.to_s
|
|
408
|
+
end
|
|
409
|
+
|
|
259
410
|
# Build the JS-global constructor map. Blocks are lazy (run at `new X()`
|
|
260
411
|
# time), so they may reference `win` / `@document` freely.
|
|
261
412
|
def build_constructors
|
|
@@ -281,8 +432,13 @@ module Dommy
|
|
|
281
432
|
# `new Document()` — a fresh empty document (content type application/xml
|
|
282
433
|
# per the DOM Standard, so it behaves as a non-HTML document).
|
|
283
434
|
"Document" => Bridge::Constructor.new do
|
|
284
|
-
Document.new(nil,
|
|
435
|
+
Document.new(nil, backend_doc: Backend.empty_xml_document).tap { |d| d.content_type = "application/xml" }
|
|
285
436
|
end,
|
|
437
|
+
# `new Text(data?)` / `new Comment(data?)` / `new DocumentFragment()` —
|
|
438
|
+
# create the node in this window's associated document (DOM Standard).
|
|
439
|
+
"Text" => Bridge::Constructor.new { |args| win.document.create_text_node(node_data_arg(args)) },
|
|
440
|
+
"Comment" => Bridge::Constructor.new { |args| win.document.create_comment(node_data_arg(args)) },
|
|
441
|
+
"DocumentFragment" => Bridge::Constructor.new { |_args| win.document.create_document_fragment },
|
|
286
442
|
"Event" => Bridge::Constructor.new { |args| Event.new(args[0], args[1]) },
|
|
287
443
|
"CustomEvent" => Bridge::Constructor.new { |args| CustomEvent.new(args[0], args[1]) },
|
|
288
444
|
"MessageEvent" => Bridge::Constructor.new { |args| MessageEvent.new(args[0], args[1]) },
|
|
@@ -290,8 +446,16 @@ module Dommy
|
|
|
290
446
|
"CloseEvent" => Bridge::Constructor.new { |args| CloseEvent.new(args[0], args[1]) },
|
|
291
447
|
"MouseEvent" => Bridge::Constructor.new { |args| MouseEvent.new(args[0], args[1]) },
|
|
292
448
|
"KeyboardEvent" => Bridge::Constructor.new { |args| KeyboardEvent.new(args[0], args[1]) },
|
|
449
|
+
"PromiseRejectionEvent" => Bridge::Constructor.new { |args| PromiseRejectionEvent.new(args[0], args[1]) },
|
|
450
|
+
"ErrorEvent" => Bridge::Constructor.new { |args| ErrorEvent.new(args[0], args[1]) },
|
|
293
451
|
"EventTarget" => Bridge::Constructor.new { |_args| StandaloneEventTarget.new },
|
|
294
452
|
"Error" => Bridge::Constructor.new { |args| ErrorValue.new(args[0]) },
|
|
453
|
+
# The host PromiseConstructor backs Ruby-side promises (fetch, the
|
|
454
|
+
# scheduler bridge). It must NOT shadow the engine's native Promise on the
|
|
455
|
+
# JS side, though — `window.Promise` is forced to globalThis.Promise in
|
|
456
|
+
# host_runtime.js's exposeConstructorsOnWindow so feature detection
|
|
457
|
+
# (core-js et al.) sees a real Promise (=== globalThis.Promise) and does
|
|
458
|
+
# not swap in a polyfill whose microtasks the host can't flush.
|
|
295
459
|
"Promise" => Bridge::PromiseConstructor.new(win),
|
|
296
460
|
"MutationObserver" => Bridge::Constructor.new { |args| MutationObserver.new(win, args[0]) },
|
|
297
461
|
"AbortController" => Bridge::Constructor.new { |_args| AbortController.new },
|
|
@@ -352,6 +516,30 @@ module Dommy
|
|
|
352
516
|
"URLPattern" => Bridge::Constructor.new { |args| URLPattern.new(args[0], args[1]) },
|
|
353
517
|
"Range" => Bridge::Constructor.new { |_args| Range.new(@document) },
|
|
354
518
|
"URL" => url,
|
|
519
|
+
# Legacy named constructors (HTML `[LegacyFactoryFunction]`): each builds
|
|
520
|
+
# the corresponding element. `new Image()` is `<img>`, `new Audio()` is
|
|
521
|
+
# `<audio>` (preload="auto"), `new Option()` is `<option>`. The JS side
|
|
522
|
+
# exposes the globals with the target interface's prototype so e.g.
|
|
523
|
+
# `new Image() instanceof HTMLImageElement` holds.
|
|
524
|
+
"Image" => Bridge::Constructor.new { |args|
|
|
525
|
+
img = win.document.create_element("img")
|
|
526
|
+
img.set_attribute("width", args[0].to_s) unless args[0].nil?
|
|
527
|
+
img.set_attribute("height", args[1].to_s) unless args[1].nil?
|
|
528
|
+
img
|
|
529
|
+
},
|
|
530
|
+
"Audio" => Bridge::Constructor.new { |args|
|
|
531
|
+
audio = win.document.create_element("audio")
|
|
532
|
+
audio.set_attribute("preload", "auto")
|
|
533
|
+
audio.set_attribute("src", args[0].to_s) unless args[0].nil?
|
|
534
|
+
audio
|
|
535
|
+
},
|
|
536
|
+
"Option" => Bridge::Constructor.new { |args|
|
|
537
|
+
opt = win.document.create_element("option")
|
|
538
|
+
opt.text = args[0].to_s unless args[0].nil? || args[0].to_s.empty?
|
|
539
|
+
opt.value = args[1].to_s if args.length >= 2 && !args[1].nil?
|
|
540
|
+
opt.selected = true if args[2] || args[3] # defaultSelected / selectedness
|
|
541
|
+
opt
|
|
542
|
+
},
|
|
355
543
|
}
|
|
356
544
|
end
|
|
357
545
|
end
|
data/lib/dommy/worker.rb
CHANGED
|
@@ -35,10 +35,13 @@ module Dommy
|
|
|
35
35
|
return if @terminated
|
|
36
36
|
|
|
37
37
|
cloned = Dommy.structured_clone(data)
|
|
38
|
-
|
|
38
|
+
# The "post message" task source — a task, not a microtask (a real worker
|
|
39
|
+
# delivers across the agent boundary in a later event-loop turn).
|
|
40
|
+
@window.scheduler.set_timeout(
|
|
39
41
|
proc do
|
|
40
42
|
@worker_side_handlers.each { |h| invoke(h, [{"data" => cloned}]) }
|
|
41
|
-
end
|
|
43
|
+
end,
|
|
44
|
+
0
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
nil
|
|
@@ -63,10 +66,11 @@ module Dommy
|
|
|
63
66
|
# Worker-side: deliver a message to the main-side `message` event.
|
|
64
67
|
def __test_post_to_main__(data)
|
|
65
68
|
cloned = Dommy.structured_clone(data)
|
|
66
|
-
@window.scheduler.
|
|
69
|
+
@window.scheduler.set_timeout(
|
|
67
70
|
proc do
|
|
68
71
|
dispatch_event(MessageEvent.new("message", "data" => cloned))
|
|
69
|
-
end
|
|
72
|
+
end,
|
|
73
|
+
0
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
nil
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "data_uri"
|
|
4
5
|
|
|
5
6
|
module Dommy
|
|
6
7
|
# `XMLHttpRequest` polyfill. Consults the same stub maps that
|
|
@@ -68,7 +69,8 @@ module Dommy
|
|
|
68
69
|
def open(method, url, async = true, _user = nil, _password = nil)
|
|
69
70
|
reset_state
|
|
70
71
|
@method = method.to_s.upcase
|
|
71
|
-
|
|
72
|
+
# XHR resolves the request URL against the document base URL, like fetch.
|
|
73
|
+
@url = @window.__internal_resolve_url__(url.to_s)
|
|
72
74
|
@async = async.nil? ? true : !!async
|
|
73
75
|
@request_headers = {}
|
|
74
76
|
transition(OPENED)
|
|
@@ -95,12 +97,30 @@ module Dommy
|
|
|
95
97
|
gen = @generation
|
|
96
98
|
dispatch_event(ProgressEvent.new("loadstart"))
|
|
97
99
|
|
|
98
|
-
entry = lookup_stub
|
|
100
|
+
entry = data_uri_entry || lookup_stub
|
|
99
101
|
track_globals
|
|
100
102
|
|
|
103
|
+
# Async (live network): the handler returns a deferred whose response
|
|
104
|
+
# arrives off-thread and is delivered on the page thread. The request stays
|
|
105
|
+
# open until then. The sync path (stubs / cache / data:) delivers inline.
|
|
106
|
+
if entry.respond_to?(:on_complete)
|
|
107
|
+
entry.on_complete { |resolved| deliver_resolved(resolved, gen) }
|
|
108
|
+
return nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
deliver_resolved(entry, gen)
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Deliver a resolved response entry (nil -> 404), honoring abort/reopen (the
|
|
116
|
+
# generation may have changed) and a simulated `delay`. Shared by the
|
|
117
|
+
# synchronous path and an async deferred completion.
|
|
118
|
+
def deliver_resolved(entry, gen)
|
|
119
|
+
return unless active?(gen)
|
|
120
|
+
|
|
101
121
|
if entry.nil?
|
|
102
122
|
deliver(body: "not found", status: 404, status_text: "Not Found", headers: {})
|
|
103
|
-
return
|
|
123
|
+
return
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
delay = entry["delay"]
|
|
@@ -111,8 +131,6 @@ module Dommy
|
|
|
111
131
|
else
|
|
112
132
|
deliver_entry(entry)
|
|
113
133
|
end
|
|
114
|
-
|
|
115
|
-
nil
|
|
116
134
|
end
|
|
117
135
|
|
|
118
136
|
def abort
|
|
@@ -288,13 +306,36 @@ module Dommy
|
|
|
288
306
|
dispatch_event(Event.new("readystatechange"))
|
|
289
307
|
end
|
|
290
308
|
|
|
309
|
+
# A `data:` URI resolves to a synthetic 200 stub entry (see Dommy::DataUri),
|
|
310
|
+
# so XHR to an inline `data:image/svg+xml;…` works like a browser. nil for any
|
|
311
|
+
# non-data URL.
|
|
312
|
+
def data_uri_entry
|
|
313
|
+
decoded = DataUri.parse(@url)
|
|
314
|
+
return nil unless decoded
|
|
315
|
+
|
|
316
|
+
{"body" => decoded[:body], "status" => 200, "statusText" => "OK",
|
|
317
|
+
"contentType" => decoded[:content_type]}
|
|
318
|
+
end
|
|
319
|
+
|
|
291
320
|
def lookup_stub
|
|
321
|
+
# Same resolution order as FetchFn: a `__fetch_handler__` callable
|
|
322
|
+
# (e.g. dommy-rack's NetworkBridge serving same-origin requests from
|
|
323
|
+
# the Rack app) gets first refusal; nil falls through to the stubs.
|
|
324
|
+
handler = @window.globals["__fetch_handler__"]
|
|
325
|
+
if handler.respond_to?(:call)
|
|
326
|
+
entry = handler.call(
|
|
327
|
+
@url,
|
|
328
|
+
{"method" => @method, "headers" => @request_headers, "body" => @request_body&.to_s}
|
|
329
|
+
)
|
|
330
|
+
return entry if entry
|
|
331
|
+
end
|
|
332
|
+
|
|
292
333
|
stub_map = @window.globals["__fetchy_stub__"] ||
|
|
293
334
|
@window.globals["__resource_fetch_stub__"] ||
|
|
294
335
|
@window.globals["__inject_fetch_stub__"]
|
|
295
336
|
return nil unless stub_map.is_a?(Hash)
|
|
296
337
|
|
|
297
|
-
stub_map[@url]
|
|
338
|
+
stub_map[@url] || stub_map[@window.__internal_url_path__(@url)]
|
|
298
339
|
end
|
|
299
340
|
|
|
300
341
|
# Bookkeeping that lets specs assert "fetch was called N times"
|
data/lib/dommy.rb
CHANGED
|
@@ -15,13 +15,24 @@ require_relative "dommy/event"
|
|
|
15
15
|
require_relative "dommy/scheduler"
|
|
16
16
|
require_relative "dommy/mutation_observer"
|
|
17
17
|
require_relative "dommy/promise"
|
|
18
|
+
require_relative "dommy/deferred_response"
|
|
18
19
|
require_relative "dommy/blob"
|
|
19
20
|
require_relative "dommy/data_transfer"
|
|
20
21
|
require_relative "dommy/crypto"
|
|
21
22
|
require_relative "dommy/text_codec"
|
|
22
23
|
require_relative "dommy/internal/observable_callback"
|
|
24
|
+
require_relative "dommy/internal/aria_role"
|
|
25
|
+
require_relative "dommy/internal/accessible_name"
|
|
26
|
+
require_relative "dommy/internal/accessible_description"
|
|
27
|
+
require_relative "dommy/internal/aria_state"
|
|
28
|
+
require_relative "dommy/internal/accessibility_tree"
|
|
29
|
+
require_relative "dommy/internal/aria_snapshot"
|
|
23
30
|
require_relative "dommy/internal/css_pseudo_handlers"
|
|
31
|
+
require_relative "dommy/internal/css_rule_text"
|
|
32
|
+
require_relative "dommy/internal/selector_ast"
|
|
24
33
|
require_relative "dommy/internal/selector_parser"
|
|
34
|
+
require_relative "dommy/internal/selector_index"
|
|
35
|
+
require_relative "dommy/internal/selector_matcher"
|
|
25
36
|
require_relative "dommy/internal/punycode"
|
|
26
37
|
require_relative "dommy/internal/idna"
|
|
27
38
|
require_relative "dommy/internal/ipv4_parser"
|
|
@@ -29,6 +40,7 @@ require_relative "dommy/intersection_observer"
|
|
|
29
40
|
require_relative "dommy/resize_observer"
|
|
30
41
|
require_relative "dommy/performance_observer"
|
|
31
42
|
require_relative "dommy/internal/range_text_serializer"
|
|
43
|
+
require_relative "dommy/internal/xml_serialization"
|
|
32
44
|
require_relative "dommy/range"
|
|
33
45
|
require_relative "dommy/animation"
|
|
34
46
|
require_relative "dommy/bridge"
|
|
@@ -51,6 +63,7 @@ require_relative "dommy/worker"
|
|
|
51
63
|
require_relative "dommy/location"
|
|
52
64
|
require_relative "dommy/history"
|
|
53
65
|
require_relative "dommy/navigator"
|
|
66
|
+
require_relative "dommy/screen"
|
|
54
67
|
require_relative "dommy/parser"
|
|
55
68
|
require_relative "dommy/attr"
|
|
56
69
|
require_relative "dommy/window"
|
|
@@ -67,6 +80,28 @@ require_relative "dommy/url"
|
|
|
67
80
|
require_relative "dommy/form_data"
|
|
68
81
|
require_relative "dommy/dom_parser"
|
|
69
82
|
require_relative "dommy/css"
|
|
83
|
+
require_relative "dommy/resources"
|
|
84
|
+
require_relative "dommy/interaction"
|
|
85
|
+
|
|
86
|
+
# JS-runtime host layer (engine-agnostic): the Runtime port + backend registry,
|
|
87
|
+
# script-boot orchestration, and the standalone Browser. A concrete JS engine
|
|
88
|
+
# (e.g. dommy-js-quickjs) registers itself through `Dommy::Js.register_runtime`.
|
|
89
|
+
require_relative "dommy/js/runtime"
|
|
90
|
+
require_relative "dommy/js/import_map"
|
|
91
|
+
require_relative "dommy/js/module_loader"
|
|
92
|
+
require_relative "dommy/js/script_boot"
|
|
93
|
+
# Engine-agnostic JS<->Ruby DOM bridge: the marshalling core (HostBridge) and
|
|
94
|
+
# its collaborators, plus the JS-side runtime bundles. A concrete backend
|
|
95
|
+
# (dommy-js-quickjs) plugs an engine in underneath via the backend contract
|
|
96
|
+
# documented in HostBridge.
|
|
97
|
+
require_relative "dommy/js/wire_tags"
|
|
98
|
+
require_relative "dommy/js/handle_table"
|
|
99
|
+
require_relative "dommy/js/marshaller"
|
|
100
|
+
require_relative "dommy/js/dom_interfaces"
|
|
101
|
+
require_relative "dommy/js/constructor_resolver"
|
|
102
|
+
require_relative "dommy/js/custom_element_bridge"
|
|
103
|
+
require_relative "dommy/js/host_bridge"
|
|
104
|
+
require_relative "dommy/browser"
|
|
70
105
|
|
|
71
106
|
module Dommy
|
|
72
107
|
# Parse an HTML string and return a fresh `Window`.
|
|
@@ -81,7 +116,7 @@ module Dommy
|
|
|
81
116
|
def self.parse(html)
|
|
82
117
|
s = html.to_s
|
|
83
118
|
if s.match?(/\A\s*(<!doctype|<html\b)/i)
|
|
84
|
-
Window.new(nil,
|
|
119
|
+
Window.new(nil, backend_doc: Backend.parse(s))
|
|
85
120
|
else
|
|
86
121
|
window = Window.new
|
|
87
122
|
window.document.body.inner_html = s
|