dommy 0.16.0 → 0.17.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/lib/dommy/animation.rb +1 -0
- data/lib/dommy/backend.rb +3 -0
- data/lib/dommy/blob.rb +6 -0
- data/lib/dommy/browser.rb +12 -4
- data/lib/dommy/character_data.rb +1 -13
- data/lib/dommy/cookie_store.rb +2 -1
- data/lib/dommy/document.rb +67 -30
- data/lib/dommy/element.rb +11 -0
- data/lib/dommy/element_internals.rb +12 -0
- data/lib/dommy/encodings/legacy_multibyte.rb +800 -0
- data/lib/dommy/encodings/multibyte_overrides.rb +76 -0
- data/lib/dommy/encodings/prescan.rb +234 -0
- data/lib/dommy/encodings.rb +121 -57
- data/lib/dommy/event.rb +107 -54
- data/lib/dommy/event_source.rb +1 -0
- data/lib/dommy/fetch.rb +244 -53
- data/lib/dommy/file_reader.rb +1 -0
- data/lib/dommy/form_data.rb +22 -9
- data/lib/dommy/form_entry_list.rb +3 -3
- data/lib/dommy/html_canvas_element.rb +85 -12
- data/lib/dommy/html_elements/base.rb +1 -0
- data/lib/dommy/html_elements/forms/input.rb +3 -0
- data/lib/dommy/html_elements/interactive.rb +56 -48
- data/lib/dommy/html_elements/metadata.rb +23 -0
- data/lib/dommy/interaction/form_submission.rb +24 -42
- data/lib/dommy/internal/accessibility_tree.rb +149 -162
- data/lib/dommy/internal/accessibility_visibility.rb +16 -17
- data/lib/dommy/internal/accessible_name.rb +231 -246
- data/lib/dommy/internal/aria_role.rb +182 -195
- data/lib/dommy/internal/aria_snapshot.rb +35 -38
- data/lib/dommy/internal/aria_state.rb +102 -118
- data/lib/dommy/internal/autofill.rb +75 -75
- data/lib/dommy/internal/backend_prefilter.rb +127 -130
- data/lib/dommy/internal/child_navigable.rb +86 -83
- data/lib/dommy/internal/close_watcher_manager.rb +1 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +64 -64
- data/lib/dommy/internal/css/calc.rb +90 -71
- data/lib/dommy/internal/css/cascade.rb +69 -67
- data/lib/dommy/internal/css/color.rb +249 -187
- data/lib/dommy/internal/css/computed_style_builder.rb +34 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +50 -11
- data/lib/dommy/internal/css/counters.rb +67 -67
- data/lib/dommy/internal/css/custom_properties.rb +114 -114
- data/lib/dommy/internal/css/parser.rb +277 -239
- data/lib/dommy/internal/css/property_registry.rb +321 -302
- data/lib/dommy/internal/css/renderability.rb +37 -35
- data/lib/dommy/internal/css/rule_index.rb +2 -2
- data/lib/dommy/internal/css/rule_index_inputs.rb +2 -2
- data/lib/dommy/internal/css/supports.rb +115 -113
- data/lib/dommy/internal/css/ua_stylesheet.rb +55 -53
- data/lib/dommy/internal/css_rule_text.rb +121 -119
- data/lib/dommy/internal/css_source.rb +1 -3
- data/lib/dommy/internal/css_syntax.rb +157 -92
- data/lib/dommy/internal/declarative_shadow_roots.rb +1 -0
- data/lib/dommy/internal/directionality.rb +120 -123
- data/lib/dommy/internal/document_live_ranges.rb +26 -1
- data/lib/dommy/internal/dom_matching.rb +185 -184
- data/lib/dommy/internal/element_geometry.rb +8 -1
- data/lib/dommy/internal/element_matching.rb +71 -69
- data/lib/dommy/internal/element_shadow.rb +1 -13
- data/lib/dommy/internal/element_state.rb +255 -265
- data/lib/dommy/internal/event_handlers.rb +66 -64
- data/lib/dommy/internal/exception_report.rb +88 -86
- data/lib/dommy/internal/focusability.rb +205 -205
- data/lib/dommy/internal/global_functions.rb +49 -47
- data/lib/dommy/internal/idl_reflection.rb +99 -97
- data/lib/dommy/internal/js_number.rb +35 -33
- data/lib/dommy/internal/light_dismiss.rb +130 -132
- data/lib/dommy/internal/literal_lookup.rb +75 -75
- data/lib/dommy/internal/mime_type.rb +54 -49
- data/lib/dommy/internal/mutation_coordinator.rb +46 -0
- data/lib/dommy/internal/mutation_observer_microtask.rb +60 -0
- data/lib/dommy/internal/namespaces.rb +51 -49
- data/lib/dommy/internal/node_adopter.rb +25 -1
- data/lib/dommy/internal/node_equality.rb +61 -59
- data/lib/dommy/internal/post_insertion_steps.rb +1 -0
- data/lib/dommy/internal/referrer.rb +144 -0
- data/lib/dommy/internal/rendered_text/collector.rb +22 -3
- data/lib/dommy/internal/rendered_text/computed_box.rb +3 -2
- data/lib/dommy/internal/rendered_text/fragment.rb +37 -38
- data/lib/dommy/internal/rendered_text/items.rb +7 -5
- data/lib/dommy/internal/selector_matcher.rb +361 -359
- data/lib/dommy/internal/selector_parser.rb +74 -62
- data/lib/dommy/internal/sequential_focus_navigation.rb +150 -151
- data/lib/dommy/internal/shadow_root_registry.rb +7 -0
- data/lib/dommy/internal/slot_assignment.rb +117 -0
- data/lib/dommy/internal/slot_signals.rb +125 -0
- data/lib/dommy/internal/unsafe_html.rb +79 -77
- data/lib/dommy/internal/url_parser.rb +5 -29
- data/lib/dommy/internal/web_idl.rb +158 -153
- data/lib/dommy/internal/window_bound.rb +14 -0
- data/lib/dommy/internal/window_constructors.rb +3 -0
- data/lib/dommy/internal/xml_serialization.rb +298 -296
- data/lib/dommy/js/dom_interfaces.rb +13 -1
- data/lib/dommy/js/host_bridge.rb +23 -1
- data/lib/dommy/js/host_callbacks.rb +7 -0
- data/lib/dommy/js/host_runtime.js +625 -61
- data/lib/dommy/js/import_map.rb +1 -1
- data/lib/dommy/js/observable_runtime.js +295 -128
- data/lib/dommy/js/webidl_members.js +1 -1
- data/lib/dommy/js/webidl_signatures.js +35 -6
- data/lib/dommy/media_query_list.rb +1 -0
- data/lib/dommy/message_channel.rb +2 -0
- data/lib/dommy/mutation_observer.rb +25 -29
- data/lib/dommy/navigator.rb +4 -1
- data/lib/dommy/node.rb +4 -0
- data/lib/dommy/notification.rb +1 -0
- data/lib/dommy/promise.rb +12 -0
- data/lib/dommy/range.rb +20 -3
- data/lib/dommy/shadow_root.rb +25 -1
- data/lib/dommy/streams.rb +18 -0
- data/lib/dommy/style_declaration.rb +15 -4
- data/lib/dommy/text_codec.rb +7 -5
- data/lib/dommy/tree_walker.rb +68 -61
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +1 -0
- data/lib/dommy/window.rb +22 -4
- data/lib/dommy/worker.rb +1 -0
- data/lib/dommy/xml_http_request.rb +12 -2
- data/lib/dommy.rb +17 -2
- metadata +11 -3
data/lib/dommy/fetch.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require "weakref"
|
|
5
6
|
require_relative "data_uri"
|
|
7
|
+
require_relative "internal/referrer"
|
|
6
8
|
|
|
7
9
|
module Dommy
|
|
8
10
|
# `fetch` polyfill. No real network — instead resolves a response
|
|
@@ -36,8 +38,30 @@ module Dommy
|
|
|
36
38
|
# Per Fetch, the request URL is resolved against the document base URL up
|
|
37
39
|
# front; the handler, stub maps, and response.url all see the absolute
|
|
38
40
|
# URL (no per-handler resolution).
|
|
39
|
-
|
|
40
|
-
init
|
|
41
|
+
input, init = args
|
|
42
|
+
# fetch(request, init) is a fetch of `new Request(request, init)`: the
|
|
43
|
+
# request's URL, and its members as far as `init` leaves them.
|
|
44
|
+
body_source = nil
|
|
45
|
+
if input.is_a?(Request)
|
|
46
|
+
given = init.is_a?(Hash) ? init.transform_keys(&:to_s) : {}
|
|
47
|
+
# A null (or undefined) body in `init` leaves the request's own.
|
|
48
|
+
given.delete("body") if given["body"].nil? || given["body"].equal?(Bridge::UNDEFINED)
|
|
49
|
+
body_source = input unless given.key?("body")
|
|
50
|
+
init = input.__internal_init__.merge(given)
|
|
51
|
+
input = input.url
|
|
52
|
+
end
|
|
53
|
+
url = @window.__internal_resolve_url__(input.to_s, utf8: true)
|
|
54
|
+
init = normalize_init(init || {})
|
|
55
|
+
begin
|
|
56
|
+
add_referer(url, init)
|
|
57
|
+
rescue Bridge::TypeError => e
|
|
58
|
+
return PromiseValue.reject(@window, e)
|
|
59
|
+
end
|
|
60
|
+
# Sending the request's body uses it up, as reading it would — once the
|
|
61
|
+
# new Request's members are valid, as its constructor takes it last.
|
|
62
|
+
if body_source && !body_source.__internal_take_body__
|
|
63
|
+
return PromiseValue.reject(@window, Bridge::TypeError.new("Failed to execute 'fetch': Request body is already used"))
|
|
64
|
+
end
|
|
41
65
|
# A cross-origin request always carries an Origin header (even for GET,
|
|
42
66
|
# which normalize_init omits it for same-origin GETs).
|
|
43
67
|
if cross_origin?(url) && !header?(init["headers"], "origin")
|
|
@@ -53,6 +77,16 @@ module Dommy
|
|
|
53
77
|
@window.globals["__last_body__"] = init["body"] if init.is_a?(Hash)
|
|
54
78
|
|
|
55
79
|
promise = PromiseValue.new(@window)
|
|
80
|
+
# An aborted signal rejects the fetch with its reason: before anything is
|
|
81
|
+
# sent when it already is, else whenever it aborts until the response
|
|
82
|
+
# arrives (the request has reached the app by then, as it would a server).
|
|
83
|
+
if (signal = request_signal(init))
|
|
84
|
+
return promise.reject(signal.reason) if signal.aborted?
|
|
85
|
+
|
|
86
|
+
abort_fetch = proc { promise.reject(signal.reason) }
|
|
87
|
+
signal.__internal_add_abort_algorithm__(abort_fetch)
|
|
88
|
+
promise.__internal_when_settled__ { signal.__internal_remove_abort_algorithm__(abort_fetch) }
|
|
89
|
+
end
|
|
56
90
|
# `mode: "same-origin"` forbids a cross-origin request — it is a network
|
|
57
91
|
# error before any fetch happens.
|
|
58
92
|
if (init["mode"] || "cors").to_s == "same-origin" && cross_origin?(url)
|
|
@@ -270,8 +304,11 @@ module Dommy
|
|
|
270
304
|
end
|
|
271
305
|
|
|
272
306
|
def deliver_entry(promise, entry, url, init)
|
|
307
|
+
# Aborted while the response was on its way.
|
|
308
|
+
return if promise.settled?
|
|
309
|
+
|
|
273
310
|
if entry.nil?
|
|
274
|
-
promise
|
|
311
|
+
fulfill_response(promise, Response.new(@window, body: "not found", status: 404, status_text: "Not Found", type: "basic"), init)
|
|
275
312
|
return
|
|
276
313
|
end
|
|
277
314
|
|
|
@@ -292,13 +329,30 @@ module Dommy
|
|
|
292
329
|
if (delay = entry["delay"])
|
|
293
330
|
install_delayed_resolve(promise, body, status, status_text, headers, init, delay)
|
|
294
331
|
else
|
|
295
|
-
|
|
332
|
+
fulfill_response(
|
|
333
|
+
promise,
|
|
296
334
|
Response.new(@window, body: body, status: status, status_text: status_text,
|
|
297
|
-
headers: headers, url: response_url, redirected: redirected, type: entry["type"] || "basic")
|
|
335
|
+
headers: headers, url: response_url, redirected: redirected, type: entry["type"] || "basic"),
|
|
336
|
+
init
|
|
298
337
|
)
|
|
299
338
|
end
|
|
300
339
|
end
|
|
301
340
|
|
|
341
|
+
# The fetch's AbortSignal (`init.signal`, a Request's own), or nil.
|
|
342
|
+
def request_signal(init)
|
|
343
|
+
signal = init["signal"]
|
|
344
|
+
signal if signal.is_a?(AbortSignal)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Fulfill the fetch with its response, whose body an abort of the signal
|
|
348
|
+
# errors from then on (Fetch's "abort the fetch() call" with a response).
|
|
349
|
+
def fulfill_response(promise, response, init)
|
|
350
|
+
if (signal = request_signal(init))
|
|
351
|
+
response.__internal_follow_abort__(signal)
|
|
352
|
+
end
|
|
353
|
+
promise.fulfill(response)
|
|
354
|
+
end
|
|
355
|
+
|
|
302
356
|
# Resolve the response entry for a request: a `__fetch_handler__`
|
|
303
357
|
# callable gets first refusal; a nil from it (or no handler) falls
|
|
304
358
|
# through to the stub maps. Each spec file installs its stub under
|
|
@@ -378,6 +432,19 @@ module Dommy
|
|
|
378
432
|
headers
|
|
379
433
|
end
|
|
380
434
|
|
|
435
|
+
# The Referer the page's request carries (Referrer Policy), in place of
|
|
436
|
+
# any the caller gave: a forbidden request header is the browser's to set.
|
|
437
|
+
def add_referer(url, init)
|
|
438
|
+
headers = init["headers"]
|
|
439
|
+
headers.delete_if { |name, _| name.to_s.casecmp?("referer") }
|
|
440
|
+
referer = Internal::Referrer.header_value(
|
|
441
|
+
@window, url,
|
|
442
|
+
referrer: Internal::Referrer.request_referrer(@window, init["referrer"]),
|
|
443
|
+
policy: Internal::Referrer.request_policy(init["referrerPolicy"])
|
|
444
|
+
)
|
|
445
|
+
headers["Referer"] = referer if referer
|
|
446
|
+
end
|
|
447
|
+
|
|
381
448
|
def request_origin
|
|
382
449
|
@window.location.__js_get__("origin").to_s
|
|
383
450
|
rescue StandardError
|
|
@@ -417,7 +484,7 @@ module Dommy
|
|
|
417
484
|
end
|
|
418
485
|
|
|
419
486
|
def safe_content_type?(value)
|
|
420
|
-
CORS_SAFE_CONTENT_TYPES.include?(
|
|
487
|
+
CORS_SAFE_CONTENT_TYPES.include?(Internal::MimeType.essence(value))
|
|
421
488
|
end
|
|
422
489
|
|
|
423
490
|
# Run the CORS preflight (an OPTIONS carrying Access-Control-Request-Method /
|
|
@@ -528,32 +595,22 @@ module Dommy
|
|
|
528
595
|
nil
|
|
529
596
|
end
|
|
530
597
|
|
|
598
|
+
# Fulfill after a simulated `delay`. An abort before then rejects the fetch
|
|
599
|
+
# (see #__js_call__) and clears the timer, so it does not linger in the
|
|
600
|
+
# scheduler's queue.
|
|
531
601
|
def install_delayed_resolve(promise, body, status, status_text, headers, init, delay_ms)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
# The timer is cleared in that path so it doesn't leak through
|
|
535
|
-
# the test scheduler's drain loop.
|
|
536
|
-
cancelled = [false]
|
|
602
|
+
signal = request_signal(init)
|
|
603
|
+
clear_timer = nil
|
|
537
604
|
timer_id = @window.scheduler.set_timeout(
|
|
538
605
|
lambda do |*_args|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
promise.fulfill(Response.new(@window, body: body, status: status, status_text: status_text, headers: headers, type: "basic"))
|
|
606
|
+
signal&.__internal_remove_abort_algorithm__(clear_timer)
|
|
607
|
+
fulfill_response(promise, Response.new(@window, body: body, status: status, status_text: status_text, headers: headers, type: "basic"), init)
|
|
542
608
|
end,
|
|
543
609
|
delay_ms.to_i
|
|
544
610
|
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
window_ref = @window
|
|
549
|
-
abort_cb = lambda do |*_args|
|
|
550
|
-
cancelled[0] = true
|
|
551
|
-
window_ref.scheduler.clear_timeout(timer_id)
|
|
552
|
-
err = ErrorValue.new("aborted", name: "AbortError")
|
|
553
|
-
promise.reject(err)
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
signal.__js_call__("addEventListener", ["abort", abort_cb])
|
|
611
|
+
scheduler = @window.scheduler
|
|
612
|
+
clear_timer = proc { scheduler.clear_timeout(timer_id) }
|
|
613
|
+
signal&.__internal_add_abort_algorithm__(clear_timer)
|
|
557
614
|
end
|
|
558
615
|
end
|
|
559
616
|
|
|
@@ -613,10 +670,7 @@ module Dommy
|
|
|
613
670
|
|
|
614
671
|
# `Request` polyfill — minimal Fetch API Request object so callers
|
|
615
672
|
# constructing `new Request(url, init)` get a value with `.url`,
|
|
616
|
-
# `.method`, `.headers`, `.body
|
|
617
|
-
# consume it directly (it still takes `(url, init)`), but having
|
|
618
|
-
# Request available means JS code that constructs one before
|
|
619
|
-
# passing to fetch keeps working.
|
|
673
|
+
# `.method`, `.headers`, `.body`, which `fetch(request)` sends.
|
|
620
674
|
class Request
|
|
621
675
|
attr_reader :url, :method, :body
|
|
622
676
|
|
|
@@ -650,9 +704,11 @@ module Dommy
|
|
|
650
704
|
# `request.signal.removeEventListener` crash.
|
|
651
705
|
sig = opts["signal"] || opts[:signal]
|
|
652
706
|
@signal = sig.is_a?(AbortSignal) ? sig : AbortSignal.new
|
|
707
|
+
@referrer = Internal::Referrer.request_referrer(window, opts["referrer"] || opts[:referrer])
|
|
708
|
+
@referrer_policy = Internal::Referrer.request_policy(opts["referrerPolicy"] || opts[:referrerPolicy])
|
|
653
709
|
end
|
|
654
710
|
|
|
655
|
-
attr_reader :headers, :credentials, :mode, :cache, :redirect, :signal
|
|
711
|
+
attr_reader :headers, :credentials, :mode, :cache, :redirect, :signal, :referrer, :referrer_policy
|
|
656
712
|
|
|
657
713
|
def __js_get__(key)
|
|
658
714
|
case key
|
|
@@ -664,6 +720,8 @@ module Dommy
|
|
|
664
720
|
@headers
|
|
665
721
|
when "body"
|
|
666
722
|
@body
|
|
723
|
+
when "bodyUsed"
|
|
724
|
+
@body_used == true
|
|
667
725
|
when "credentials"
|
|
668
726
|
@credentials
|
|
669
727
|
when "mode"
|
|
@@ -674,6 +732,10 @@ module Dommy
|
|
|
674
732
|
@redirect
|
|
675
733
|
when "signal"
|
|
676
734
|
@signal
|
|
735
|
+
when "referrer"
|
|
736
|
+
@referrer
|
|
737
|
+
when "referrerPolicy"
|
|
738
|
+
@referrer_policy
|
|
677
739
|
else
|
|
678
740
|
Bridge::ABSENT
|
|
679
741
|
end
|
|
@@ -685,25 +747,14 @@ module Dommy
|
|
|
685
747
|
def __js_call__(method, _args)
|
|
686
748
|
case method
|
|
687
749
|
when "clone"
|
|
688
|
-
Request
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
"method" => @method,
|
|
692
|
-
"body" => @body,
|
|
693
|
-
"headers" => @headers.to_h,
|
|
694
|
-
"credentials" => @credentials,
|
|
695
|
-
"mode" => @mode,
|
|
696
|
-
"cache" => @cache,
|
|
697
|
-
"redirect" => @redirect,
|
|
698
|
-
"signal" => @signal
|
|
699
|
-
},
|
|
700
|
-
@window
|
|
701
|
-
)
|
|
750
|
+
raise Bridge::TypeError, "Failed to execute 'clone' on 'Request': Request body is already used" if @body_used
|
|
751
|
+
|
|
752
|
+
Request.new(@url, __internal_init__, @window)
|
|
702
753
|
when "text"
|
|
703
754
|
consume_body { immediate(Response.utf8_decode(@body_bytes)) }
|
|
704
755
|
when "json"
|
|
705
756
|
consume_body do
|
|
706
|
-
immediate(JSON.parse(Response.utf8_decode(@body_bytes)))
|
|
757
|
+
immediate(JSON.parse(Response.utf8_decode(@body_bytes), **Response::JSON_OPTIONS))
|
|
707
758
|
rescue JSON::ParserError => e
|
|
708
759
|
rejected(ErrorValue.new("JSON parse: #{e.message}"))
|
|
709
760
|
end
|
|
@@ -719,19 +770,49 @@ module Dommy
|
|
|
719
770
|
end
|
|
720
771
|
end
|
|
721
772
|
|
|
773
|
+
# fetch(request) sends the body: false when it was already used, else it
|
|
774
|
+
# is used now. A request with no body has nothing to use up.
|
|
775
|
+
def __internal_take_body__
|
|
776
|
+
return true unless body?
|
|
777
|
+
return false if @body_used
|
|
778
|
+
|
|
779
|
+
@body_used = true
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# The request as a `RequestInit`: what `clone()` builds a copy from, and
|
|
783
|
+
# what `fetch(request, init)` starts from before `init` overrides it.
|
|
784
|
+
def __internal_init__
|
|
785
|
+
{
|
|
786
|
+
"method" => @method,
|
|
787
|
+
"body" => @body,
|
|
788
|
+
"headers" => @headers.to_h,
|
|
789
|
+
"credentials" => @credentials,
|
|
790
|
+
"mode" => @mode,
|
|
791
|
+
"cache" => @cache,
|
|
792
|
+
"redirect" => @redirect,
|
|
793
|
+
"signal" => @signal,
|
|
794
|
+
"referrer" => @referrer,
|
|
795
|
+
"referrerPolicy" => @referrer_policy
|
|
796
|
+
}
|
|
797
|
+
end
|
|
798
|
+
|
|
722
799
|
private
|
|
723
800
|
|
|
724
801
|
# WHATWG Body: the body can be consumed once. A second consume rejects rather
|
|
725
|
-
# than throwing synchronously.
|
|
802
|
+
# than throwing synchronously. A request with no body has nothing to use
|
|
803
|
+
# up: reading it leaves `bodyUsed` false, and it can be read and cloned
|
|
804
|
+
# again.
|
|
726
805
|
def consume_body
|
|
727
806
|
if @body_used
|
|
728
807
|
return rejected(ErrorValue.new("Failed to read body: body stream already read", name: "TypeError"))
|
|
729
808
|
end
|
|
730
809
|
|
|
731
|
-
@body_used = true
|
|
810
|
+
@body_used = true if body?
|
|
732
811
|
yield
|
|
733
812
|
end
|
|
734
813
|
|
|
814
|
+
def body? = !@body.nil? && !@body.equal?(Bridge::UNDEFINED)
|
|
815
|
+
|
|
735
816
|
def immediate(value)
|
|
736
817
|
@window ? PromiseValue.resolve(@window, value) : value
|
|
737
818
|
end
|
|
@@ -772,6 +853,7 @@ module Dommy
|
|
|
772
853
|
@type = type
|
|
773
854
|
@has_body = has_body ? true : false
|
|
774
855
|
@body_used = false
|
|
856
|
+
@body_error = nil
|
|
775
857
|
@body_stream = nil
|
|
776
858
|
end
|
|
777
859
|
|
|
@@ -909,6 +991,10 @@ module Dommy
|
|
|
909
991
|
end
|
|
910
992
|
end
|
|
911
993
|
|
|
994
|
+
# ECMAScript's JSON.parse, which a page's JSON goes through, keeps the last
|
|
995
|
+
# of duplicate keys; json 3 rejects them unless told otherwise.
|
|
996
|
+
JSON_OPTIONS = {allow_duplicate_key: true}.freeze
|
|
997
|
+
|
|
912
998
|
# WHATWG "UTF-8 decode" for a body's text(): interpret the raw bytes as
|
|
913
999
|
# UTF-8, replacing any ill-formed sequence with U+FFFD, and drop a single
|
|
914
1000
|
# leading byte-order mark (U+FEFF). Well-formed UTF-8 (the common case) is
|
|
@@ -1002,6 +1088,68 @@ module Dommy
|
|
|
1002
1088
|
Bridge::UNHANDLED
|
|
1003
1089
|
end
|
|
1004
1090
|
|
|
1091
|
+
# The response's fetch can still be aborted through `signal` (Fetch's
|
|
1092
|
+
# "abort the fetch() call" with a response): a body not yet read errors
|
|
1093
|
+
# with the abort reason, for a consume method and for a reader of `body`
|
|
1094
|
+
# alike. The signal lets go once the body is read — by a consume method,
|
|
1095
|
+
# or a reader taking it to its end — and holds the response and its body
|
|
1096
|
+
# stream only weakly until then, so a long-lived signal does not keep a
|
|
1097
|
+
# response nobody reads (`fetch(url, {signal}).then(r => r.ok)`).
|
|
1098
|
+
def __internal_follow_abort__(signal)
|
|
1099
|
+
return unless body_readable?
|
|
1100
|
+
|
|
1101
|
+
@abort_signal = signal
|
|
1102
|
+
@abort_body = BodyAbort.new(self, signal)
|
|
1103
|
+
signal.__internal_remove_abort_algorithms_if__ { |algorithm| algorithm.is_a?(BodyAbort) && algorithm.gone? }
|
|
1104
|
+
signal.__internal_add_abort_algorithm__(@abort_body)
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# The abort algorithm a Response leaves on its fetch's signal. It errors
|
|
1108
|
+
# the body when the response is still around, else the body stream a
|
|
1109
|
+
# reader may still hold; once both have been collected there is nothing
|
|
1110
|
+
# left to abort, and the signal drops it the next time it takes one.
|
|
1111
|
+
class BodyAbort
|
|
1112
|
+
def initialize(response, signal)
|
|
1113
|
+
@response = WeakRef.new(response)
|
|
1114
|
+
@stream = nil
|
|
1115
|
+
@signal = signal
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def stream=(stream)
|
|
1119
|
+
@stream = WeakRef.new(stream)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def gone?
|
|
1123
|
+
!alive(@response) && !alive(@stream)
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# The stream was read to its end or errored: nothing left to abort.
|
|
1127
|
+
def release
|
|
1128
|
+
@signal.__internal_remove_abort_algorithm__(self)
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def call
|
|
1132
|
+
if (response = alive(@response))
|
|
1133
|
+
response.__internal_abort_body__(@signal.reason)
|
|
1134
|
+
elsif (stream = alive(@stream)) && stream.state == :readable
|
|
1135
|
+
stream.__internal_error__(@signal.reason)
|
|
1136
|
+
end
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
private
|
|
1140
|
+
|
|
1141
|
+
def alive(ref)
|
|
1142
|
+
ref&.weakref_alive? ? ref.__getobj__ : nil
|
|
1143
|
+
rescue WeakRef::RefError
|
|
1144
|
+
nil
|
|
1145
|
+
end
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# Fetch "error response's body" from the signal's abort algorithm.
|
|
1149
|
+
def __internal_abort_body__(reason)
|
|
1150
|
+
abort_body(reason)
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1005
1153
|
include Bridge::Methods
|
|
1006
1154
|
include FormDataBody
|
|
1007
1155
|
js_methods %w[text json arrayBuffer blob formData clone]
|
|
@@ -1011,7 +1159,7 @@ module Dommy
|
|
|
1011
1159
|
consume_body { immediate(Response.utf8_decode(@body)) }
|
|
1012
1160
|
when "json"
|
|
1013
1161
|
consume_body do
|
|
1014
|
-
immediate(JSON.parse(scrub_lone_surrogates(@body)))
|
|
1162
|
+
immediate(JSON.parse(scrub_lone_surrogates(@body), **Response::JSON_OPTIONS))
|
|
1015
1163
|
rescue JSON::ParserError => e
|
|
1016
1164
|
rejected(ErrorValue.new("JSON parse: #{e.message}"))
|
|
1017
1165
|
end
|
|
@@ -1039,16 +1187,37 @@ module Dommy
|
|
|
1039
1187
|
end
|
|
1040
1188
|
|
|
1041
1189
|
# Run a body-consume (text/json/arrayBuffer/blob), rejecting if the body was
|
|
1042
|
-
# already used (spec returns a rejected promise, not a synchronous throw)
|
|
1190
|
+
# already used (spec returns a rejected promise, not a synchronous throw),
|
|
1191
|
+
# or with the abort reason when its fetch was aborted before it was read.
|
|
1043
1192
|
def consume_body
|
|
1044
1193
|
if body_used?
|
|
1045
1194
|
return rejected(ErrorValue.new("Failed to read body: body stream already read", name: "TypeError"))
|
|
1046
1195
|
end
|
|
1047
1196
|
|
|
1048
|
-
|
|
1197
|
+
# A null body (not an empty one) has nothing to use up.
|
|
1198
|
+
@body_used = true if @has_body
|
|
1199
|
+
@abort_signal&.__internal_remove_abort_algorithm__(@abort_body)
|
|
1200
|
+
return rejected(@body_error) if @body_error
|
|
1201
|
+
|
|
1049
1202
|
yield
|
|
1050
1203
|
end
|
|
1051
1204
|
|
|
1205
|
+
# Fetch "error response's body" when it is readable: all of a body is
|
|
1206
|
+
# there at once, so it is readable while a nonempty one is unread — an
|
|
1207
|
+
# empty one is already closed, as is a stream read to its end.
|
|
1208
|
+
def abort_body(reason)
|
|
1209
|
+
return if @body_used || !body_readable?
|
|
1210
|
+
|
|
1211
|
+
@body_error = reason
|
|
1212
|
+
@body_stream&.__internal_error__(reason)
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
def body_readable?
|
|
1216
|
+
return false unless @has_body
|
|
1217
|
+
|
|
1218
|
+
@body_stream ? @body_stream.state == :readable : !@body.empty?
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1052
1221
|
# Lazily build + memoize the body ReadableStream (a single Uint8Array chunk
|
|
1053
1222
|
# then close), or nil when there is no body.
|
|
1054
1223
|
def body_stream
|
|
@@ -1058,18 +1227,34 @@ module Dommy
|
|
|
1058
1227
|
stream = ReadableStream.new(@window)
|
|
1059
1228
|
stream.__internal_enqueue__(Bridge::Bytes.new(@body.bytes)) unless @body.empty?
|
|
1060
1229
|
stream.__internal_close__
|
|
1230
|
+
stream.__internal_error__(@body_error) if @body_error
|
|
1231
|
+
follow_abort_through(stream)
|
|
1061
1232
|
stream
|
|
1062
1233
|
end
|
|
1063
1234
|
end
|
|
1064
1235
|
|
|
1236
|
+
# A reader can outlive the response, so the abort algorithm also reaches
|
|
1237
|
+
# the stream; a reader taking it to its end leaves the signal nothing to
|
|
1238
|
+
# abort. The stream's callback is the algorithm's own method, not a block
|
|
1239
|
+
# here, which would hold the response for as long as the stream.
|
|
1240
|
+
def follow_abort_through(stream)
|
|
1241
|
+
return unless @abort_body
|
|
1242
|
+
|
|
1243
|
+
@abort_body.stream = stream
|
|
1244
|
+
stream.__internal_when_done__(&@abort_body.method(:release))
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1065
1247
|
# WHATWG: clone throws if the body is already disturbed/locked; otherwise the
|
|
1066
|
-
# clone is an independent Response over the same bytes.
|
|
1248
|
+
# clone is an independent Response over the same bytes. Its body is a tee
|
|
1249
|
+
# of this one's, so an abort of the fetch errors it too: the clone of a
|
|
1250
|
+
# body already errored is errored, and one made before the abort follows
|
|
1251
|
+
# the same signal.
|
|
1067
1252
|
def clone_response
|
|
1068
1253
|
if body_used?
|
|
1069
1254
|
raise Bridge::TypeError, "Failed to execute 'clone' on 'Response': Response body is already used."
|
|
1070
1255
|
end
|
|
1071
1256
|
|
|
1072
|
-
Response.new(
|
|
1257
|
+
copy = Response.new(
|
|
1073
1258
|
@window,
|
|
1074
1259
|
body: @body,
|
|
1075
1260
|
status: @status,
|
|
@@ -1080,6 +1265,12 @@ module Dommy
|
|
|
1080
1265
|
type: @type,
|
|
1081
1266
|
has_body: @has_body
|
|
1082
1267
|
)
|
|
1268
|
+
if @body_error
|
|
1269
|
+
copy.__internal_abort_body__(@body_error)
|
|
1270
|
+
elsif @abort_signal
|
|
1271
|
+
copy.__internal_follow_abort__(@abort_signal)
|
|
1272
|
+
end
|
|
1273
|
+
copy
|
|
1083
1274
|
end
|
|
1084
1275
|
|
|
1085
1276
|
# A run of one or more adjacent `\uXXXX` JSON escapes.
|
data/lib/dommy/file_reader.rb
CHANGED
data/lib/dommy/form_data.rb
CHANGED
|
@@ -65,12 +65,17 @@ module Dommy
|
|
|
65
65
|
copy
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
# A filename left out, as opposed to one given as nil (JS null) or
|
|
69
|
+
# undefined: WebIDL picks an overload by how many arguments a call passed.
|
|
70
|
+
NO_FILENAME = Object.new.freeze
|
|
71
|
+
private_constant :NO_FILENAME
|
|
72
|
+
|
|
73
|
+
def append(name, value, filename = NO_FILENAME)
|
|
69
74
|
@pairs << [name.to_s, entry_value(value, filename)]
|
|
70
75
|
nil
|
|
71
76
|
end
|
|
72
77
|
|
|
73
|
-
def set(name, value, filename =
|
|
78
|
+
def set(name, value, filename = NO_FILENAME)
|
|
74
79
|
key = name.to_s
|
|
75
80
|
v = entry_value(value, filename)
|
|
76
81
|
replaced = false
|
|
@@ -160,9 +165,9 @@ module Dommy
|
|
|
160
165
|
def __js_call__(method, args)
|
|
161
166
|
case method
|
|
162
167
|
when "append"
|
|
163
|
-
append(args[0], args[1], args
|
|
168
|
+
append(args[0], args[1], args.fetch(2, NO_FILENAME))
|
|
164
169
|
when "set"
|
|
165
|
-
set(args[0], args[1], args
|
|
170
|
+
set(args[0], args[1], args.fetch(2, NO_FILENAME))
|
|
166
171
|
when "get"
|
|
167
172
|
get(args[0])
|
|
168
173
|
when "getAll"
|
|
@@ -186,14 +191,22 @@ module Dommy
|
|
|
186
191
|
|
|
187
192
|
# XHR's "create an entry": a Blob that is not a File becomes a File named
|
|
188
193
|
# "blob", and a File given a filename one of that name, so every file
|
|
189
|
-
# entry is a File with a name; anything else is a string.
|
|
194
|
+
# entry is a File with a name; anything else is a string. A call with a
|
|
195
|
+
# third argument, even undefined, can only be the (name, Blob, filename)
|
|
196
|
+
# overload, so a string value with one is a TypeError. The filename is
|
|
197
|
+
# optional: undefined is its absence, null the string "null".
|
|
190
198
|
def entry_value(value, filename)
|
|
191
|
-
|
|
199
|
+
unless value.is_a?(Blob)
|
|
200
|
+
raise Bridge::TypeError, "parameter 2 is not of type 'Blob'." unless filename.equal?(NO_FILENAME)
|
|
201
|
+
|
|
202
|
+
return Internal::WebIDL.dom_string(value)
|
|
203
|
+
end
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
return value if value.is_a?(File) && filename.nil?
|
|
205
|
+
return value if value.is_a?(File) && default_filename?(filename)
|
|
195
206
|
|
|
196
|
-
value.__internal_file_named__(filename
|
|
207
|
+
value.__internal_file_named__(default_filename?(filename) ? "blob" : Internal::WebIDL.dom_string(filename))
|
|
197
208
|
end
|
|
209
|
+
|
|
210
|
+
def default_filename?(filename) = filename.equal?(NO_FILENAME) || filename.equal?(Bridge::UNDEFINED)
|
|
198
211
|
end
|
|
199
212
|
end
|
|
@@ -7,9 +7,9 @@ module Dommy
|
|
|
7
7
|
# (Dommy::Interaction::FormSubmission) and `new FormData(form)` build on this,
|
|
8
8
|
# so the two paths collect and fire identically.
|
|
9
9
|
class FormEntryList
|
|
10
|
-
# @param encoding [
|
|
10
|
+
# @param encoding [String] the submission encoding's name, used for a
|
|
11
11
|
# hidden `_charset_`. `new FormData(form)` uses UTF-8.
|
|
12
|
-
def initialize(form, submitter: nil, encoding:
|
|
12
|
+
def initialize(form, submitter: nil, encoding: "UTF-8")
|
|
13
13
|
@form = form
|
|
14
14
|
@submitter = submitter
|
|
15
15
|
@encoding = encoding
|
|
@@ -115,7 +115,7 @@ module Dommy
|
|
|
115
115
|
collect_file(el, name, data)
|
|
116
116
|
when "hidden"
|
|
117
117
|
# A hidden `_charset_` reports the submission encoding.
|
|
118
|
-
data.append(name, name.casecmp?("_charset_") ? @encoding
|
|
118
|
+
data.append(name, name.casecmp?("_charset_") ? @encoding : el.value.to_s)
|
|
119
119
|
else
|
|
120
120
|
data.append(name, el.value.to_s)
|
|
121
121
|
end
|