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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# A lightweight test browser: parse HTML, build window/document, run its
|
|
5
|
+
# classic `<script>` tags (inline + external via a resources adapter), fire
|
|
6
|
+
# DOMContentLoaded/load, and collect JS errors / console output. For
|
|
7
|
+
# standalone HTML + JS (bundled SPA, fixture HTML); the Rack/Rails entry point
|
|
8
|
+
# is `Dommy::Rack::Session` (a later phase).
|
|
9
|
+
#
|
|
10
|
+
# Dommy::Browser.open(html, resources: Dommy::Resources.static("/app.js" => "...")) do |b|
|
|
11
|
+
# b.settle
|
|
12
|
+
# b.evaluate('document.querySelector("h1").textContent')
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# JS errors are not swallowed: in strict mode (default) any unhandled rejection
|
|
16
|
+
# or uncaught script error fails at the next checkpoint (after boot, after
|
|
17
|
+
# `settle`, at dispose). Wrap intentional errors in `allow_js_errors { … }`.
|
|
18
|
+
class Browser
|
|
19
|
+
# Capybara-vocabulary finding / scoping / field interaction / click /
|
|
20
|
+
# matchers come from the shared interaction layer; each interaction's events
|
|
21
|
+
# are dispatched Ruby-side (synchronously invoking JS handlers), then
|
|
22
|
+
# `after_interaction` drains the runtime's microtasks so promise reactions
|
|
23
|
+
# settle before the next line.
|
|
24
|
+
include Dommy::Interaction::Driver
|
|
25
|
+
|
|
26
|
+
# Raised in strict mode when JS errors were collected and not acknowledged.
|
|
27
|
+
class JsError < StandardError
|
|
28
|
+
attr_reader :causes
|
|
29
|
+
|
|
30
|
+
def initialize(causes)
|
|
31
|
+
@causes = causes
|
|
32
|
+
super(build_message(causes))
|
|
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
|
|
42
|
+
|
|
43
|
+
attr_reader :window, :runtime, :js_errors, :console
|
|
44
|
+
|
|
45
|
+
# Build a browser and (unless `execute_scripts: false`) boot its scripts. In
|
|
46
|
+
# block form the browser is yielded and disposed afterward, returning the
|
|
47
|
+
# block value.
|
|
48
|
+
def self.open(html, **opts)
|
|
49
|
+
browser = new(html, **opts)
|
|
50
|
+
return browser unless block_given?
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
yield browser
|
|
54
|
+
ensure
|
|
55
|
+
browser.dispose
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true,
|
|
60
|
+
wasm_memory_shim: false, backend: nil)
|
|
61
|
+
@resources = resources
|
|
62
|
+
@strict = strict
|
|
63
|
+
@js_errors = []
|
|
64
|
+
@console = []
|
|
65
|
+
@acknowledged = 0
|
|
66
|
+
@allow_errors = false
|
|
67
|
+
@disposed = false
|
|
68
|
+
|
|
69
|
+
@window = Dommy.parse(html)
|
|
70
|
+
@window.location.__internal_set_url__(url) if url
|
|
71
|
+
|
|
72
|
+
# The JS engine is pluggable: `backend:` selects a registered runtime
|
|
73
|
+
# (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
|
|
74
|
+
@runtime = Js.build_runtime(backend)
|
|
75
|
+
@runtime.on_unhandled_rejection { |err| @js_errors << err }
|
|
76
|
+
@runtime.on_callback_error { |err| @js_errors << err } if @runtime.respond_to?(:on_callback_error)
|
|
77
|
+
@runtime.on_log { |log| @console << log }
|
|
78
|
+
@runtime.define_host_object("document", @window.document)
|
|
79
|
+
@runtime.install_window(@window)
|
|
80
|
+
@runtime.install_browser_globals
|
|
81
|
+
# Opt-in WPT scaffolding (common/sab.js derives SharedArrayBuffer through
|
|
82
|
+
# WebAssembly.Memory); off by default so real pages don't see the shim.
|
|
83
|
+
@runtime.install_wasm_memory_shim if wasm_memory_shim && @runtime.respond_to?(:install_wasm_memory_shim)
|
|
84
|
+
@window.globals["__fetch_handler__"] = Resources::FetchHandler.new(@resources) if @resources
|
|
85
|
+
|
|
86
|
+
if execute_scripts
|
|
87
|
+
doc = @window.document
|
|
88
|
+
# Dynamically-inserted `<script src>` (webpack/Vite on-demand chunks)
|
|
89
|
+
# fetch + run through the same resources adapter, after boot.
|
|
90
|
+
doc.external_script_runner = lambda do |element, src|
|
|
91
|
+
Js::ScriptBoot.run_external_script(@runtime, doc, element, src,
|
|
92
|
+
resources: @resources, on_error: ->(e) { @js_errors << e })
|
|
93
|
+
end
|
|
94
|
+
Js::ScriptBoot.run_document_scripts(
|
|
95
|
+
@runtime, doc, resources: @resources, on_error: ->(e) { @js_errors << e }
|
|
96
|
+
)
|
|
97
|
+
# Leave the page in a ready state: run on-load promises, due-now timers,
|
|
98
|
+
# and rAF (not future timers). `settle: false` observes it mid-flight.
|
|
99
|
+
@runtime.settle if settle
|
|
100
|
+
end
|
|
101
|
+
check_js_errors!
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def document = @window.document
|
|
105
|
+
|
|
106
|
+
# Current document HTML (serialized).
|
|
107
|
+
def html = @window.document.document_element&.outer_html
|
|
108
|
+
|
|
109
|
+
# Evaluate an expression / statement body and return the decoded value.
|
|
110
|
+
def evaluate(js)
|
|
111
|
+
result = @runtime.evaluate(js)
|
|
112
|
+
check_js_errors!
|
|
113
|
+
result
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Run JS for side effects.
|
|
117
|
+
def execute(js)
|
|
118
|
+
@runtime.execute(js)
|
|
119
|
+
check_js_errors!
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Settle the work ready at the current virtual time: drain microtasks, run
|
|
124
|
+
# due-now timers, flush requestAnimationFrame. Does NOT fire a future
|
|
125
|
+
# `setTimeout(300)` — use `advance_time(300)` for debounce/throttle.
|
|
126
|
+
def settle
|
|
127
|
+
@runtime.settle
|
|
128
|
+
check_js_errors!
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Advance virtual time by `ms`, running timers that come due, then settle.
|
|
133
|
+
def advance_time(ms)
|
|
134
|
+
@window.scheduler.advance_time(ms)
|
|
135
|
+
@runtime.drain_microtasks
|
|
136
|
+
check_js_errors!
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# An interaction's events have been dispatched (Ruby-side, synchronously
|
|
141
|
+
# invoking JS handlers); drain the runtime's microtasks so promise reactions
|
|
142
|
+
# land before the next line, then enforce strict mode.
|
|
143
|
+
def after_interaction
|
|
144
|
+
@runtime.drain_microtasks
|
|
145
|
+
check_js_errors!
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Click a submit-capable button. The button's click event fires (JS may
|
|
149
|
+
# handle / preventDefault it); if it is an un-prevented submit button, the
|
|
150
|
+
# form's `submit` event is dispatched too (a SPA's JS handles it). Real
|
|
151
|
+
# navigation on an un-prevented submit is a Session concern (out of scope).
|
|
152
|
+
def click_button(locator)
|
|
153
|
+
button = finder.find_button(locator)
|
|
154
|
+
prevented = Dommy::Interaction::EventSynthesis.click(button)
|
|
155
|
+
if !prevented && submit_button?(button) && (form = finder.form_for(button))
|
|
156
|
+
form.dispatch_event(Dommy::Event.new("submit", "bubbles" => true, "cancelable" => true))
|
|
157
|
+
end
|
|
158
|
+
after_interaction
|
|
159
|
+
button
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Click a link, firing its click event so SPA JS (Turbo, React Router, …)
|
|
163
|
+
# can intercept. Real navigation on an un-prevented click is out of scope.
|
|
164
|
+
def click_link(locator)
|
|
165
|
+
link = finder.find_link(locator)
|
|
166
|
+
Dommy::Interaction::EventSynthesis.click(link)
|
|
167
|
+
after_interaction
|
|
168
|
+
link
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Suppress strict-mode failure for JS errors raised inside the block (they
|
|
172
|
+
# stay collected in #js_errors for inspection). For tests that expect errors.
|
|
173
|
+
def allow_js_errors
|
|
174
|
+
prev = @allow_errors
|
|
175
|
+
@allow_errors = true
|
|
176
|
+
yield
|
|
177
|
+
ensure
|
|
178
|
+
@allow_errors = prev
|
|
179
|
+
@acknowledged = @js_errors.length
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def dispose
|
|
183
|
+
return if @disposed
|
|
184
|
+
|
|
185
|
+
@disposed = true
|
|
186
|
+
pending = unacknowledged
|
|
187
|
+
@runtime&.dispose
|
|
188
|
+
raise JsError, pending if @strict && !pending.empty?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def unacknowledged = @js_errors[@acknowledged..] || []
|
|
194
|
+
|
|
195
|
+
def submit_button?(button)
|
|
196
|
+
if button.tag_name == "BUTTON"
|
|
197
|
+
button.type == "submit"
|
|
198
|
+
else
|
|
199
|
+
%w[submit image].include?(button.type)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# In strict mode, fail on any JS error collected since the last
|
|
204
|
+
# acknowledgement. Marks all current errors acknowledged so each is reported
|
|
205
|
+
# at most once.
|
|
206
|
+
def check_js_errors!
|
|
207
|
+
return if @allow_errors
|
|
208
|
+
return unless @strict
|
|
209
|
+
|
|
210
|
+
pending = unacknowledged
|
|
211
|
+
return if pending.empty?
|
|
212
|
+
|
|
213
|
+
@acknowledged = @js_errors.length
|
|
214
|
+
raise JsError, pending
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/dommy/crypto.rb
CHANGED
|
@@ -59,6 +59,8 @@ module Dommy
|
|
|
59
59
|
case key
|
|
60
60
|
when "subtle"
|
|
61
61
|
subtle
|
|
62
|
+
else
|
|
63
|
+
Bridge::ABSENT
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
|
|
@@ -396,6 +398,8 @@ module Dommy
|
|
|
396
398
|
{"name" => @algorithm_name, "hash" => {"name" => @hash_name}}
|
|
397
399
|
when "usages"
|
|
398
400
|
@usages
|
|
401
|
+
else
|
|
402
|
+
Bridge::ABSENT
|
|
399
403
|
end
|
|
400
404
|
end
|
|
401
405
|
end
|