dommy 0.12.0 → 0.13.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/bridge.rb +16 -3
- data/lib/dommy/browser.rb +43 -47
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/compression_streams.rb +12 -62
- data/lib/dommy/document.rb +5 -5
- data/lib/dommy/element.rb +2 -3
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +299 -0
- data/lib/dommy/event.rb +4 -5
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +10 -0
- data/lib/dommy/html_elements.rb +136 -62
- data/lib/dommy/internal/exception_report.rb +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- data/lib/dommy/internal/url_parser.rb +70 -18
- data/lib/dommy/js/dom_interfaces.rb +1 -0
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/host_bridge.rb +13 -0
- data/lib/dommy/js/host_runtime.js +107 -17
- data/lib/dommy/js/marshaller.rb +21 -2
- data/lib/dommy/js/runtime.rb +8 -1
- data/lib/dommy/js/script_boot.rb +58 -14
- data/lib/dommy/js/wire_tags.rb +8 -0
- data/lib/dommy/location.rb +14 -4
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/promise.rb +4 -4
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/url.rb +34 -32
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +162 -34
- data/lib/dommy/xml_http_request.rb +18 -4
- data/lib/dommy.rb +2 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64a22d9ba17102a03881baedbda66b5236b8c4499140d779e4c4acba86583241
|
|
4
|
+
data.tar.gz: e111a114c5a7ae9485a3e99af339e70a59dce6b9a83a837639ac6564873a8d4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67c651bd499d2cd0d6cf1fb862b11a9f0f6b68ba1814abe40732b990e5b66b9b5f0494fd33cb989e4b7a5f64faa3a8a152e8979231f4c082f32187999188030a
|
|
7
|
+
data.tar.gz: e7e8ee9194687b9fe6be28646e9f80d32631f3cf78e5b9fefbab5fd913b9a1d6116ab819bae3a54030f2fefc6e4fa8e478c8ab2667cc086e009c5ebb1a7c9481
|
data/lib/dommy/bridge.rb
CHANGED
|
@@ -57,9 +57,20 @@ module Dommy
|
|
|
57
57
|
class JSValue
|
|
58
58
|
attr_reader :ref
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
# `stack_frames` are the value's JS stack, when the tag that crossed
|
|
61
|
+
# carried one. Ruby cannot reach through a ref to read `.stack`, so an
|
|
62
|
+
# error-shaped value brings its frames with it (see host_runtime.js
|
|
63
|
+
# tagValue) and reporting uses them for the source position.
|
|
64
|
+
attr_reader :stack_frames
|
|
65
|
+
|
|
66
|
+
# The value's JS constructor name, when the tag carried one.
|
|
67
|
+
attr_reader :js_name
|
|
68
|
+
|
|
69
|
+
def initialize(ref, label = nil, stack_frames = nil, js_name = nil)
|
|
61
70
|
@ref = ref
|
|
62
71
|
@label = label
|
|
72
|
+
@stack_frames = stack_frames
|
|
73
|
+
@js_name = js_name
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
def to_s = (@label || "[object]").to_s
|
|
@@ -75,9 +86,11 @@ module Dommy
|
|
|
75
86
|
class ThrowValue < RuntimeError
|
|
76
87
|
attr_reader :value
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
# `message` overrides the value's string form, for a caller that can say
|
|
90
|
+
# more about it than `to_s` can (a JS error's kind, say).
|
|
91
|
+
def initialize(value, message = nil)
|
|
79
92
|
@value = value
|
|
80
|
-
super(value.to_s)
|
|
93
|
+
super(message || value.to_s)
|
|
81
94
|
end
|
|
82
95
|
end
|
|
83
96
|
|
data/lib/dommy/browser.rb
CHANGED
|
@@ -23,24 +23,14 @@ module Dommy
|
|
|
23
23
|
# settle before the next line.
|
|
24
24
|
include Dommy::Interaction::Driver
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
class JsError < StandardError
|
|
28
|
-
attr_reader :causes
|
|
26
|
+
attr_reader :window, :runtime, :console
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def build_message(causes)
|
|
38
|
-
lines = causes.map { |e| " #{e.class}: #{e.message}" }
|
|
39
|
-
"#{causes.length} uncaught JS error(s):\n#{lines.join("\n")}"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
28
|
+
# The ledger of errors the page left unhandled (`Dommy::Js::ErrorLog`);
|
|
29
|
+
# shared with the other hosts so strict mode behaves the same everywhere.
|
|
30
|
+
attr_reader :error_log
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
# The errors themselves, as a browser console's scrollback.
|
|
33
|
+
def js_errors = @error_log.errors
|
|
44
34
|
|
|
45
35
|
# Build a browser and (unless `execute_scripts: false`) boot its scripts. In
|
|
46
36
|
# block form the browser is yielded and disposed afterward, returning the
|
|
@@ -72,16 +62,13 @@ module Dommy
|
|
|
72
62
|
wasm_memory_shim: false, backend: nil, navigable: false, same_origin: false)
|
|
73
63
|
@resources = resources
|
|
74
64
|
@same_origin = same_origin
|
|
75
|
-
@strict = strict
|
|
76
65
|
@backend = backend
|
|
77
66
|
@execute_scripts = execute_scripts
|
|
78
67
|
@settle_after_boot = settle
|
|
79
68
|
@wasm_memory_shim = wasm_memory_shim
|
|
80
69
|
@navigable = navigable
|
|
81
|
-
@
|
|
70
|
+
@error_log = Js::ErrorLog.new(strict: strict)
|
|
82
71
|
@console = []
|
|
83
|
-
@acknowledged = 0
|
|
84
|
-
@allow_errors = false
|
|
85
72
|
@disposed = false
|
|
86
73
|
@pending_navigation = nil
|
|
87
74
|
@runtime = nil
|
|
@@ -238,22 +225,19 @@ module Dommy
|
|
|
238
225
|
|
|
239
226
|
# Suppress strict-mode failure for JS errors raised inside the block (they
|
|
240
227
|
# stay collected in #js_errors for inspection). For tests that expect errors.
|
|
241
|
-
def allow_js_errors
|
|
242
|
-
|
|
243
|
-
@allow_errors = true
|
|
244
|
-
yield
|
|
245
|
-
ensure
|
|
246
|
-
@allow_errors = prev
|
|
247
|
-
@acknowledged = @js_errors.length
|
|
228
|
+
def allow_js_errors(&block)
|
|
229
|
+
@error_log.allow(&block)
|
|
248
230
|
end
|
|
249
231
|
|
|
232
|
+
# Tear the realm down, then fail on anything the page left unhandled and
|
|
233
|
+
# nobody has reported. Disposing first means the failure cannot leave a live
|
|
234
|
+
# VM behind.
|
|
250
235
|
def dispose
|
|
251
236
|
return if @disposed
|
|
252
237
|
|
|
253
238
|
@disposed = true
|
|
254
|
-
pending = unacknowledged
|
|
255
239
|
@runtime&.dispose
|
|
256
|
-
|
|
240
|
+
@error_log.check!(context: current_url)
|
|
257
241
|
end
|
|
258
242
|
|
|
259
243
|
private
|
|
@@ -268,8 +252,19 @@ module Dommy
|
|
|
268
252
|
# The JS engine is pluggable: `@backend` selects a registered runtime
|
|
269
253
|
# (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
|
|
270
254
|
runtime = Js.build_runtime(@backend)
|
|
271
|
-
|
|
272
|
-
|
|
255
|
+
# Every uncaught error the page produces goes through the window's
|
|
256
|
+
# "report an exception" / "notify about rejected promises" funnel, and only
|
|
257
|
+
# what the page left unhandled lands here. A page that installs its own
|
|
258
|
+
# `window.onerror` / `unhandledrejection` handler and cancels the event
|
|
259
|
+
# suppresses the failure, exactly as it would in a browser.
|
|
260
|
+
window.__internal_on_unhandled_error__ { |err| @error_log.record(err) }
|
|
261
|
+
# A rejection the page handled after we reported it is retracted, so it
|
|
262
|
+
# stops failing anything (WHATWG `rejectionhandled`).
|
|
263
|
+
window.__internal_on_rejection_handled__ { |record| @error_log.retract(record) }
|
|
264
|
+
runtime.on_unhandled_rejection { |err| report_rejection(window, err) }
|
|
265
|
+
if runtime.respond_to?(:on_callback_error)
|
|
266
|
+
runtime.on_callback_error { |err| Internal::ExceptionReport.report_at(window, err) }
|
|
267
|
+
end
|
|
273
268
|
runtime.on_log { |log| @console << log }
|
|
274
269
|
runtime.define_host_object("document", window.document)
|
|
275
270
|
runtime.install_window(window)
|
|
@@ -285,24 +280,35 @@ module Dommy
|
|
|
285
280
|
# Installed whenever a runtime is attached — an embedder that drives script
|
|
286
281
|
# boot itself (`execute_scripts: false`) still needs inline handlers wired.
|
|
287
282
|
doc.inline_handler_wirer = lambda do
|
|
288
|
-
Js::ScriptBoot.wire_inline_handlers(runtime, on_error: ->(e) { @
|
|
283
|
+
Js::ScriptBoot.wire_inline_handlers(runtime, on_error: ->(e) { @error_log.record(e) })
|
|
289
284
|
end
|
|
290
285
|
return unless @execute_scripts
|
|
291
286
|
|
|
287
|
+
# `on_error:` here is only the windowless fallback — a booted document has
|
|
288
|
+
# a window, so a throwing script reports through it (see ScriptBoot).
|
|
289
|
+
|
|
292
290
|
# Dynamically-inserted `<script src>` (webpack/Vite on-demand chunks)
|
|
293
291
|
# fetch + run through the same resources adapter, after boot.
|
|
294
292
|
doc.external_script_runner = lambda do |element, src|
|
|
295
293
|
Js::ScriptBoot.run_external_script(runtime, doc, element, src,
|
|
296
|
-
resources: @resources, on_error: ->(e) { @
|
|
294
|
+
resources: @resources, on_error: ->(e) { @error_log.record(e) })
|
|
297
295
|
end
|
|
298
296
|
Js::ScriptBoot.run_document_scripts(
|
|
299
|
-
runtime, doc, resources: @resources, on_error: ->(e) { @
|
|
297
|
+
runtime, doc, resources: @resources, on_error: ->(e) { @error_log.record(e) }
|
|
300
298
|
)
|
|
301
299
|
# Leave the page in a ready state: run on-load promises, due-now timers,
|
|
302
300
|
# and rAF (not future timers). `settle: false` observes it mid-flight.
|
|
303
301
|
runtime.settle if @settle_after_boot
|
|
304
302
|
end
|
|
305
303
|
|
|
304
|
+
# Route an unhandled promise rejection through the page's own
|
|
305
|
+
# `unhandledrejection` handling first. The engine decides WHEN a rejection
|
|
306
|
+
# counts as unhandled (see Window#__internal_report_rejection__).
|
|
307
|
+
def report_rejection(window, error)
|
|
308
|
+
value = Internal::ExceptionReport.error_value(error)
|
|
309
|
+
window.__internal_report_rejection__(value, host_error: error)
|
|
310
|
+
end
|
|
311
|
+
|
|
306
312
|
# Perform a recorded navigation: fetch the target (following redirects),
|
|
307
313
|
# fire the old document's unload, then replace the Window + JS realm with the
|
|
308
314
|
# freshly parsed document and update the joint history. A network miss or a
|
|
@@ -400,8 +406,6 @@ module Dommy
|
|
|
400
406
|
content_type.empty? || content_type.include?("html") || content_type.include?("xml")
|
|
401
407
|
end
|
|
402
408
|
|
|
403
|
-
def unacknowledged = @js_errors[@acknowledged..] || []
|
|
404
|
-
|
|
405
409
|
def submit_button?(button)
|
|
406
410
|
if button.tag_name == "BUTTON"
|
|
407
411
|
button.type == "submit"
|
|
@@ -410,18 +414,10 @@ module Dommy
|
|
|
410
414
|
end
|
|
411
415
|
end
|
|
412
416
|
|
|
413
|
-
# In strict mode, fail on
|
|
414
|
-
#
|
|
415
|
-
# at most once.
|
|
417
|
+
# In strict mode, fail on anything the page left unhandled since the last
|
|
418
|
+
# checkpoint. The ledger drains itself, so each error is reported once.
|
|
416
419
|
def check_js_errors!
|
|
417
|
-
|
|
418
|
-
return unless @strict
|
|
419
|
-
|
|
420
|
-
pending = unacknowledged
|
|
421
|
-
return if pending.empty?
|
|
422
|
-
|
|
423
|
-
@acknowledged = @js_errors.length
|
|
424
|
-
raise JsError, pending
|
|
420
|
+
@error_log.check!(context: current_url)
|
|
425
421
|
end
|
|
426
422
|
end
|
|
427
423
|
end
|
|
@@ -20,6 +20,22 @@ module Dommy
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Invoke `callback` so a JS throw SURFACES as a Ruby exception instead of
|
|
24
|
+
# being swallowed by the bridge. WHATWG requires a task's exception to be
|
|
25
|
+
# reported at the global ("report an exception") rather than silently
|
|
26
|
+
# dropped, so the entry points that own a task — a timer / rAF callback —
|
|
27
|
+
# invoke this form and report whatever comes out. The plain `invoke` keeps
|
|
28
|
+
# the swallowing behavior for callers that handle their own errors.
|
|
29
|
+
def invoke_raising(callback, *args)
|
|
30
|
+
return if callback.nil?
|
|
31
|
+
|
|
32
|
+
if callback.respond_to?(:__js_invoke__)
|
|
33
|
+
callback.__js_invoke__(args, raising: true)
|
|
34
|
+
else
|
|
35
|
+
invoke(callback, *args)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
23
39
|
# Invoke a DOM event listener per the EventTarget rule: an object with
|
|
24
40
|
# `handle_event`, else a Ruby callable, else a JS-bridged callable (tried in
|
|
25
41
|
# that order). A JS function listener's `this` must be the event's
|
|
@@ -10,43 +10,18 @@ module Dommy
|
|
|
10
10
|
# finalizes and emits the compressed/decompressed bytes downstream.
|
|
11
11
|
#
|
|
12
12
|
# Spec: https://wicg.github.io/compression/
|
|
13
|
-
class CompressionStream
|
|
13
|
+
class CompressionStream < TransformStream
|
|
14
14
|
SUPPORTED = %w[gzip deflate deflate-raw].freeze
|
|
15
15
|
|
|
16
|
-
attr_reader :readable, :writable
|
|
17
|
-
|
|
18
16
|
def initialize(window, format)
|
|
19
17
|
raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
buffer = +""
|
|
22
20
|
compressor = build_compressor(format.to_s)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@writable = WritableStream.new(
|
|
28
|
-
window,
|
|
29
|
-
{
|
|
30
|
-
"write" => proc { |chunk| @buffer << coerce(chunk) },
|
|
31
|
-
"close" => proc do
|
|
32
|
-
compressed = compressor.call(@buffer)
|
|
33
|
-
controller.enqueue(compressed)
|
|
34
|
-
@readable.__internal_close__
|
|
35
|
-
end,
|
|
36
|
-
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
37
|
-
}
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def __js_get__(key)
|
|
42
|
-
case key
|
|
43
|
-
when "readable"
|
|
44
|
-
@readable
|
|
45
|
-
when "writable"
|
|
46
|
-
@writable
|
|
47
|
-
else
|
|
48
|
-
Bridge::ABSENT
|
|
49
|
-
end
|
|
21
|
+
super(window, {
|
|
22
|
+
"transform" => proc { |chunk, _controller| buffer << coerce(chunk); nil },
|
|
23
|
+
"flush" => proc { |controller| controller.enqueue(compressor.call(buffer)); nil }
|
|
24
|
+
})
|
|
50
25
|
end
|
|
51
26
|
|
|
52
27
|
private
|
|
@@ -79,43 +54,18 @@ module Dommy
|
|
|
79
54
|
end
|
|
80
55
|
end
|
|
81
56
|
|
|
82
|
-
class DecompressionStream
|
|
57
|
+
class DecompressionStream < TransformStream
|
|
83
58
|
SUPPORTED = %w[gzip deflate deflate-raw].freeze
|
|
84
59
|
|
|
85
|
-
attr_reader :readable, :writable
|
|
86
|
-
|
|
87
60
|
def initialize(window, format)
|
|
88
61
|
raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
|
|
89
62
|
|
|
90
|
-
|
|
63
|
+
buffer = +""
|
|
91
64
|
decompressor = build_decompressor(format.to_s)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@writable = WritableStream.new(
|
|
97
|
-
window,
|
|
98
|
-
{
|
|
99
|
-
"write" => proc { |chunk| @buffer << coerce(chunk) },
|
|
100
|
-
"close" => proc do
|
|
101
|
-
plain = decompressor.call(@buffer)
|
|
102
|
-
controller.enqueue(plain)
|
|
103
|
-
@readable.__internal_close__
|
|
104
|
-
end,
|
|
105
|
-
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def __js_get__(key)
|
|
111
|
-
case key
|
|
112
|
-
when "readable"
|
|
113
|
-
@readable
|
|
114
|
-
when "writable"
|
|
115
|
-
@writable
|
|
116
|
-
else
|
|
117
|
-
Bridge::ABSENT
|
|
118
|
-
end
|
|
65
|
+
super(window, {
|
|
66
|
+
"transform" => proc { |chunk, _controller| buffer << coerce(chunk); nil },
|
|
67
|
+
"flush" => proc { |controller| controller.enqueue(decompressor.call(buffer)); nil }
|
|
68
|
+
})
|
|
119
69
|
end
|
|
120
70
|
|
|
121
71
|
private
|
data/lib/dommy/document.rb
CHANGED
|
@@ -810,11 +810,11 @@ module Dommy
|
|
|
810
810
|
href = base_el["href"].to_s
|
|
811
811
|
return doc_url if href.empty?
|
|
812
812
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
813
|
+
# HTML "document base URL": the frozen base URL is the href parsed
|
|
814
|
+
# against the document's URL; one that does not parse falls back.
|
|
815
|
+
URL.new(href, doc_url.to_s.empty? ? nil : doc_url).href
|
|
816
|
+
rescue Bridge::TypeError
|
|
817
|
+
doc_url
|
|
818
818
|
end
|
|
819
819
|
|
|
820
820
|
# `document.domain` — host portion of the URL. Real browsers
|
data/lib/dommy/element.rb
CHANGED
|
@@ -2623,9 +2623,8 @@ module Dommy
|
|
|
2623
2623
|
# so `a.href = "http://example.org/?ä"` reads back percent-encoded — which is
|
|
2624
2624
|
# what the URL parser produces and what `URI.join` does not.
|
|
2625
2625
|
def resolve_url(raw)
|
|
2626
|
-
|
|
2627
|
-
base =
|
|
2628
|
-
base = nil if base.to_s.empty?
|
|
2626
|
+
base = @document.base_uri.to_s
|
|
2627
|
+
base = nil if base.empty?
|
|
2629
2628
|
Internal::UrlParser.serialize(Internal::UrlParser.parse(raw.to_s, base))
|
|
2630
2629
|
rescue Internal::UrlParser::Failure
|
|
2631
2630
|
raw.to_s
|