dommy 0.8.1 → 0.10.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 +57 -13
- data/lib/dommy/backend/makiri_adapter.rb +364 -0
- data/lib/dommy/backend.rb +143 -33
- data/lib/dommy/blob.rb +50 -6
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +420 -0
- data/lib/dommy/callable_invoker.rb +6 -2
- 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 +901 -275
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +940 -353
- data/lib/dommy/event.rb +529 -17
- data/lib/dommy/fetch.rb +577 -53
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +26 -3
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +150 -27
- data/lib/dommy/html_elements.rb +1968 -282
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +385 -0
- data/lib/dommy/interaction/event_synthesis.rb +164 -0
- data/lib/dommy/interaction/field_interactor.rb +177 -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/child_node.rb +199 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +472 -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 +55 -9
- data/lib/dommy/internal/mutation_coordinator.rb +122 -11
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +217 -38
- data/lib/dommy/internal/observer_manager.rb +6 -0
- data/lib/dommy/internal/observer_matcher.rb +6 -8
- data/lib/dommy/internal/parent_node.rb +98 -57
- 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 +845 -0
- data/lib/dommy/internal/selector_parser.rb +321 -132
- 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 +101 -0
- data/lib/dommy/js/dom_interfaces.rb +177 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +538 -0
- data/lib/dommy/js/host_runtime.js +2357 -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 +301 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +78 -13
- 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 -5
- data/lib/dommy/navigation.rb +263 -0
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +198 -35
- 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 +187 -33
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +165 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +109 -13
- data/lib/dommy/storage.rb +63 -11
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +51 -46
- data/lib/dommy/url.rb +17 -3
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +45 -2
- data/lib/dommy/window.rb +322 -15
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +76 -9
- data/lib/dommy.rb +38 -1
- metadata +82 -14
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
|
@@ -0,0 +1,420 @@
|
|
|
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
|
+
# Start a navigable session by fetching the initial document from
|
|
60
|
+
# `resources`, rather than passing literal HTML. Links / forms / location
|
|
61
|
+
# then perform real cross-document navigation (fetch → replace Window + JS
|
|
62
|
+
# realm), with the browser handle (history, resources, error log) surviving.
|
|
63
|
+
# Dommy::Browser.visit("http://localhost/", resources: my_resources)
|
|
64
|
+
def self.visit(url, resources:, **opts)
|
|
65
|
+
browser = new("<!doctype html><html><head></head><body></body></html>",
|
|
66
|
+
url: "about:blank", resources: resources, navigable: true, **opts)
|
|
67
|
+
browser.visit(url, replace: true)
|
|
68
|
+
browser
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true,
|
|
72
|
+
wasm_memory_shim: false, backend: nil, navigable: false, same_origin: false)
|
|
73
|
+
@resources = resources
|
|
74
|
+
@same_origin = same_origin
|
|
75
|
+
@strict = strict
|
|
76
|
+
@backend = backend
|
|
77
|
+
@execute_scripts = execute_scripts
|
|
78
|
+
@settle_after_boot = settle
|
|
79
|
+
@wasm_memory_shim = wasm_memory_shim
|
|
80
|
+
@navigable = navigable
|
|
81
|
+
@js_errors = []
|
|
82
|
+
@console = []
|
|
83
|
+
@acknowledged = 0
|
|
84
|
+
@allow_errors = false
|
|
85
|
+
@disposed = false
|
|
86
|
+
@pending_navigation = nil
|
|
87
|
+
@runtime = nil
|
|
88
|
+
|
|
89
|
+
@window = Dommy.parse(html)
|
|
90
|
+
@window.location.__internal_set_url__(url) if url
|
|
91
|
+
install_runtime(@window)
|
|
92
|
+
|
|
93
|
+
if navigable
|
|
94
|
+
@fetcher = Navigation::Fetcher.new(@resources, same_origin: @same_origin)
|
|
95
|
+
@history = Navigation::JointHistory.new
|
|
96
|
+
@window.navigation_delegate = self
|
|
97
|
+
@history.push(current_url, window: @window, windex: @window.history.__internal_index__)
|
|
98
|
+
end
|
|
99
|
+
check_js_errors!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def document = @window.document
|
|
103
|
+
|
|
104
|
+
# Current document HTML (serialized).
|
|
105
|
+
def html = @window.document.document_element&.outer_html
|
|
106
|
+
|
|
107
|
+
# The current document's URL (the address bar).
|
|
108
|
+
def current_url = @window.location.href
|
|
109
|
+
|
|
110
|
+
# The joint (tab) history of a navigable browser, or nil for a plain
|
|
111
|
+
# single-document browser.
|
|
112
|
+
attr_reader :history
|
|
113
|
+
|
|
114
|
+
# Programmatically navigate to `url` (a Ruby-initiated visit). Only
|
|
115
|
+
# meaningful for a navigable browser; performs the fetch + document swap
|
|
116
|
+
# immediately (there is no JS on the stack).
|
|
117
|
+
def visit(url, replace: false)
|
|
118
|
+
raise "browser is not navigable (use Browser.visit or navigable: true)" unless @navigable
|
|
119
|
+
|
|
120
|
+
@pending_navigation = {url: url.to_s, method: "GET", source: :visit, replace: replace}
|
|
121
|
+
flush_navigation!
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Reload the current document (re-fetch, replace the current history entry).
|
|
126
|
+
def reload
|
|
127
|
+
visit(current_url, replace: true)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Move back / forward one entry in the joint history. A same-document target
|
|
131
|
+
# (the entry's window is still live) traverses in place (popstate); a
|
|
132
|
+
# document-boundary target is re-fetched (no bfcache — D2).
|
|
133
|
+
def back = traverse(-1)
|
|
134
|
+
def forward = traverse(1)
|
|
135
|
+
|
|
136
|
+
# --- NavigationDelegate port (see Dommy::Navigation) ---
|
|
137
|
+
|
|
138
|
+
# A cross-document navigation intent (link / form / location). Navigation is
|
|
139
|
+
# a task: rather than swap the Window + JS realm synchronously (which may run
|
|
140
|
+
# while the outgoing realm's JS is still on the stack — e.g. `location.href =`
|
|
141
|
+
# inside a script), record it and perform the fetch + swap at the next drain
|
|
142
|
+
# boundary (settle / after_interaction / advance_time). Ruby-initiated visits
|
|
143
|
+
# flush immediately since no JS is on the stack.
|
|
144
|
+
def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false)
|
|
145
|
+
@pending_navigation = {
|
|
146
|
+
url: url, method: method, body: body, params: params, enctype: enctype,
|
|
147
|
+
headers: headers, replace: replace, source: source
|
|
148
|
+
}
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A cross-document history traversal. Ruby-initiated (back / forward), so it
|
|
153
|
+
# runs immediately: a same-document target (its window is still live)
|
|
154
|
+
# traverses in place (popstate); a document-boundary target is re-fetched.
|
|
155
|
+
def traverse(delta)
|
|
156
|
+
return self unless @navigable
|
|
157
|
+
|
|
158
|
+
entry = delta.negative? ? @history.back : @history.forward
|
|
159
|
+
return self unless entry
|
|
160
|
+
|
|
161
|
+
if entry.window && entry.window.equal?(@window)
|
|
162
|
+
@window.history.__internal_go_to__(entry.windex)
|
|
163
|
+
@runtime.drain_microtasks
|
|
164
|
+
check_js_errors!
|
|
165
|
+
else
|
|
166
|
+
perform_navigation!({url: entry.url, method: "GET", source: :traverse}, rebind: true)
|
|
167
|
+
end
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Evaluate an expression / statement body and return the decoded value.
|
|
172
|
+
def evaluate(js)
|
|
173
|
+
result = @runtime.evaluate(js)
|
|
174
|
+
check_js_errors!
|
|
175
|
+
result
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Run JS for side effects.
|
|
179
|
+
def execute(js)
|
|
180
|
+
@runtime.execute(js)
|
|
181
|
+
check_js_errors!
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Settle the work ready at the current virtual time: drain microtasks, run
|
|
186
|
+
# due-now timers, flush requestAnimationFrame. Does NOT fire a future
|
|
187
|
+
# `setTimeout(300)` — use `advance_time(300)` for debounce/throttle.
|
|
188
|
+
def settle
|
|
189
|
+
@runtime.settle
|
|
190
|
+
flush_navigation!
|
|
191
|
+
check_js_errors!
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Advance virtual time by `ms`, running timers that come due, then settle.
|
|
196
|
+
def advance_time(ms)
|
|
197
|
+
@window.scheduler.advance_time(ms)
|
|
198
|
+
@runtime.drain_microtasks
|
|
199
|
+
flush_navigation!
|
|
200
|
+
check_js_errors!
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# An interaction's events have been dispatched (Ruby-side, synchronously
|
|
205
|
+
# invoking JS handlers); drain the runtime's microtasks so promise reactions
|
|
206
|
+
# land before the next line, then enforce strict mode.
|
|
207
|
+
def after_interaction
|
|
208
|
+
@runtime.drain_microtasks
|
|
209
|
+
flush_navigation!
|
|
210
|
+
check_js_errors!
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Click a submit-capable button. The button's click event fires (JS may
|
|
214
|
+
# handle / preventDefault it); if it is an un-prevented submit button, the
|
|
215
|
+
# owning form's submission algorithm runs (a real SubmitEvent a SPA can
|
|
216
|
+
# intercept, then the delegate navigation). In a navigable browser that
|
|
217
|
+
# follows the submit for real; otherwise the delegate just records it.
|
|
218
|
+
def click_button(locator)
|
|
219
|
+
button = finder.find_button(locator)
|
|
220
|
+
# An un-prevented click runs the button's activation behavior — a submit
|
|
221
|
+
# button submits its owning form (real SubmitEvent + delegate navigation),
|
|
222
|
+
# so a navigable browser follows the submit for real.
|
|
223
|
+
Dommy::Interaction::EventSynthesis.click(button)
|
|
224
|
+
after_interaction
|
|
225
|
+
button
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Click a link, firing its click event so SPA JS (Turbo, React Router, …)
|
|
229
|
+
# can intercept. An un-prevented click runs the anchor's activation behavior
|
|
230
|
+
# (follow-the-hyperlink); in a navigable browser that navigates for real,
|
|
231
|
+
# otherwise the delegate records it.
|
|
232
|
+
def click_link(locator)
|
|
233
|
+
link = finder.find_link(locator)
|
|
234
|
+
Dommy::Interaction::EventSynthesis.click(link)
|
|
235
|
+
after_interaction
|
|
236
|
+
link
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Suppress strict-mode failure for JS errors raised inside the block (they
|
|
240
|
+
# stay collected in #js_errors for inspection). For tests that expect errors.
|
|
241
|
+
def allow_js_errors
|
|
242
|
+
prev = @allow_errors
|
|
243
|
+
@allow_errors = true
|
|
244
|
+
yield
|
|
245
|
+
ensure
|
|
246
|
+
@allow_errors = prev
|
|
247
|
+
@acknowledged = @js_errors.length
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def dispose
|
|
251
|
+
return if @disposed
|
|
252
|
+
|
|
253
|
+
@disposed = true
|
|
254
|
+
pending = unacknowledged
|
|
255
|
+
@runtime&.dispose
|
|
256
|
+
raise JsError, pending if @strict && !pending.empty?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
# Build a fresh JS realm for `window`, wire error/console/fetch/external-
|
|
262
|
+
# script seams, and boot its `<script>` tags. Disposes the previous realm
|
|
263
|
+
# first (a no-op on the initial load), so a navigation tears the outgoing
|
|
264
|
+
# realm — and with it every pending timer / microtask on the old Window's
|
|
265
|
+
# scheduler — down before the new page runs.
|
|
266
|
+
def install_runtime(window)
|
|
267
|
+
@runtime&.dispose
|
|
268
|
+
# The JS engine is pluggable: `@backend` selects a registered runtime
|
|
269
|
+
# (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
|
|
270
|
+
runtime = Js.build_runtime(@backend)
|
|
271
|
+
runtime.on_unhandled_rejection { |err| @js_errors << err }
|
|
272
|
+
runtime.on_callback_error { |err| @js_errors << err } if runtime.respond_to?(:on_callback_error)
|
|
273
|
+
runtime.on_log { |log| @console << log }
|
|
274
|
+
runtime.define_host_object("document", window.document)
|
|
275
|
+
runtime.install_window(window)
|
|
276
|
+
runtime.install_browser_globals
|
|
277
|
+
# Opt-in WPT scaffolding (common/sab.js derives SharedArrayBuffer through
|
|
278
|
+
# WebAssembly.Memory); off by default so real pages don't see the shim.
|
|
279
|
+
runtime.install_wasm_memory_shim if @wasm_memory_shim && runtime.respond_to?(:install_wasm_memory_shim)
|
|
280
|
+
window.globals["__fetch_handler__"] = Resources::FetchHandler.new(@resources) if @resources
|
|
281
|
+
@runtime = runtime
|
|
282
|
+
return unless @execute_scripts
|
|
283
|
+
|
|
284
|
+
doc = window.document
|
|
285
|
+
# Dynamically-inserted `<script src>` (webpack/Vite on-demand chunks)
|
|
286
|
+
# fetch + run through the same resources adapter, after boot.
|
|
287
|
+
doc.external_script_runner = lambda do |element, src|
|
|
288
|
+
Js::ScriptBoot.run_external_script(runtime, doc, element, src,
|
|
289
|
+
resources: @resources, on_error: ->(e) { @js_errors << e })
|
|
290
|
+
end
|
|
291
|
+
Js::ScriptBoot.run_document_scripts(
|
|
292
|
+
runtime, doc, resources: @resources, on_error: ->(e) { @js_errors << e }
|
|
293
|
+
)
|
|
294
|
+
# Leave the page in a ready state: run on-load promises, due-now timers,
|
|
295
|
+
# and rAF (not future timers). `settle: false` observes it mid-flight.
|
|
296
|
+
runtime.settle if @settle_after_boot
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Perform a recorded navigation: fetch the target (following redirects),
|
|
300
|
+
# fire the old document's unload, then replace the Window + JS realm with the
|
|
301
|
+
# freshly parsed document and update the joint history. A network miss or a
|
|
302
|
+
# non-document response leaves the current page in place.
|
|
303
|
+
MAX_META_REFRESHES = 20
|
|
304
|
+
|
|
305
|
+
def perform_navigation!(nav, rebind: false, refresh_depth: 0)
|
|
306
|
+
response, final_url = @fetcher.request(
|
|
307
|
+
method: nav[:method] || "GET", url: nav[:url], params: nav[:params],
|
|
308
|
+
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {}
|
|
309
|
+
)
|
|
310
|
+
return unless response&.success? && document_response?(response)
|
|
311
|
+
|
|
312
|
+
# Fire the outgoing document's unload sequence while its realm is still
|
|
313
|
+
# alive, then surface any of its errors before the realm is torn down.
|
|
314
|
+
fire_unload(@window)
|
|
315
|
+
check_js_errors!
|
|
316
|
+
|
|
317
|
+
new_window = Dommy.parse(response.body)
|
|
318
|
+
new_window.location.__internal_set_url__(final_url)
|
|
319
|
+
new_window.navigation_delegate = self
|
|
320
|
+
@window = new_window
|
|
321
|
+
install_runtime(new_window)
|
|
322
|
+
|
|
323
|
+
windex = new_window.history.__internal_index__
|
|
324
|
+
if rebind || nav[:replace]
|
|
325
|
+
@history.rebind_current(url: final_url, window: new_window, windex: windex)
|
|
326
|
+
else
|
|
327
|
+
@history.push(final_url, window: new_window, windex: windex)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
follow_meta_refresh!(refresh_depth)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# If the freshly loaded document asks for an immediate `<meta http-equiv=
|
|
334
|
+
# refresh>`, follow it (as a replace, like a redirect), capped so a page that
|
|
335
|
+
# refreshes to itself can't loop forever.
|
|
336
|
+
def follow_meta_refresh!(depth)
|
|
337
|
+
return if depth >= MAX_META_REFRESHES
|
|
338
|
+
|
|
339
|
+
target = meta_refresh_target(@window.document)
|
|
340
|
+
return unless target
|
|
341
|
+
|
|
342
|
+
perform_navigation!({url: target, method: "GET", source: :meta_refresh},
|
|
343
|
+
rebind: true, refresh_depth: depth + 1)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# The resolved URL a `<meta http-equiv="refresh" content="0; url=…">` points
|
|
347
|
+
# at, or nil when the document has none (or a refresh with no URL, which just
|
|
348
|
+
# reloads and is left alone to avoid a busy loop).
|
|
349
|
+
def meta_refresh_target(document)
|
|
350
|
+
document.query_selector_all("meta").each do |meta|
|
|
351
|
+
next unless meta.get_attribute("http-equiv").to_s.casecmp?("refresh")
|
|
352
|
+
|
|
353
|
+
_delay, separator, rest = meta.get_attribute("content").to_s.partition(";")
|
|
354
|
+
next if separator.empty?
|
|
355
|
+
|
|
356
|
+
url = rest.strip.sub(/\Aurl\s*=\s*/i, "").gsub(/\A["']|["']\z/, "").strip
|
|
357
|
+
return resolve_against_current(url) unless url.empty?
|
|
358
|
+
end
|
|
359
|
+
nil
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def resolve_against_current(url)
|
|
363
|
+
URI.join(current_url, url).to_s
|
|
364
|
+
rescue URI::InvalidURIError
|
|
365
|
+
url
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Perform a pending navigation recorded by the delegate (JS-initiated
|
|
369
|
+
# location.href= / form submit / link click). Called at drain boundaries so
|
|
370
|
+
# the swap never runs with the outgoing realm's JS on the stack.
|
|
371
|
+
def flush_navigation!
|
|
372
|
+
return unless @navigable
|
|
373
|
+
|
|
374
|
+
nav = @pending_navigation
|
|
375
|
+
return unless nav
|
|
376
|
+
|
|
377
|
+
@pending_navigation = nil
|
|
378
|
+
perform_navigation!(nav)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def fire_unload(window)
|
|
382
|
+
window.dispatch_event(Dommy::Event.new("pagehide"))
|
|
383
|
+
window.dispatch_event(Dommy::Event.new("unload"))
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Only HTML/XML responses replace the document; other content types (a JSON
|
|
387
|
+
# API hit, an image) leave the current page. A response with no Content-Type
|
|
388
|
+
# is treated as a document (fixtures commonly omit it).
|
|
389
|
+
def document_response?(response)
|
|
390
|
+
headers = response.headers || {}
|
|
391
|
+
key = headers.keys.find { |k| k.to_s.casecmp?("content-type") }
|
|
392
|
+
content_type = key ? headers[key].to_s.downcase : ""
|
|
393
|
+
content_type.empty? || content_type.include?("html") || content_type.include?("xml")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def unacknowledged = @js_errors[@acknowledged..] || []
|
|
397
|
+
|
|
398
|
+
def submit_button?(button)
|
|
399
|
+
if button.tag_name == "BUTTON"
|
|
400
|
+
button.type == "submit"
|
|
401
|
+
else
|
|
402
|
+
%w[submit image].include?(button.type)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# In strict mode, fail on any JS error collected since the last
|
|
407
|
+
# acknowledgement. Marks all current errors acknowledged so each is reported
|
|
408
|
+
# at most once.
|
|
409
|
+
def check_js_errors!
|
|
410
|
+
return if @allow_errors
|
|
411
|
+
return unless @strict
|
|
412
|
+
|
|
413
|
+
pending = unacknowledged
|
|
414
|
+
return if pending.empty?
|
|
415
|
+
|
|
416
|
+
@acknowledged = @js_errors.length
|
|
417
|
+
raise JsError, pending
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
@@ -22,12 +22,16 @@ module Dommy
|
|
|
22
22
|
|
|
23
23
|
# Invoke a DOM event listener per the EventTarget rule: an object with
|
|
24
24
|
# `handle_event`, else a Ruby callable, else a JS-bridged callable (tried in
|
|
25
|
-
# that order).
|
|
26
|
-
|
|
25
|
+
# that order). A JS function listener's `this` must be the event's
|
|
26
|
+
# currentTarget (the node the listener is attached to), so pass it through
|
|
27
|
+
# when the bridge supports an explicit receiver.
|
|
28
|
+
def invoke_listener(listener, event, current_target = nil)
|
|
27
29
|
if listener.respond_to?(:handle_event)
|
|
28
30
|
listener.handle_event(event)
|
|
29
31
|
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
30
32
|
listener.call(event)
|
|
33
|
+
elsif listener.respond_to?(:__js_call_with_this__)
|
|
34
|
+
listener.__js_call_with_this__([event], current_target)
|
|
31
35
|
elsif listener.respond_to?(:__js_call__)
|
|
32
36
|
listener.__js_call__("call", [event])
|
|
33
37
|
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
|