dommy 0.15.0 → 0.16.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/backend.rb +16 -15
- data/lib/dommy/blob.rb +17 -0
- data/lib/dommy/bridge/values.rb +29 -2
- data/lib/dommy/bridge/wire_tags.rb +8 -4
- data/lib/dommy/browser.rb +324 -49
- data/lib/dommy/character_data.rb +20 -27
- data/lib/dommy/class_list.rb +24 -9
- data/lib/dommy/cookie_jar.rb +408 -0
- data/lib/dommy/cookie_store.rb +104 -49
- data/lib/dommy/css.rb +34 -8
- data/lib/dommy/custom_elements.rb +362 -114
- data/lib/dommy/data_transfer.rb +22 -2
- data/lib/dommy/document.rb +1071 -200
- data/lib/dommy/dom_exception.rb +6 -0
- data/lib/dommy/dom_parser.rb +46 -14
- data/lib/dommy/dom_string_list.rb +36 -0
- data/lib/dommy/element.rb +217 -102
- data/lib/dommy/element_internals.rb +215 -0
- data/lib/dommy/encodings.rb +10 -0
- data/lib/dommy/event.rb +378 -122
- data/lib/dommy/event_source.rb +4 -30
- data/lib/dommy/file_reader.rb +5 -29
- data/lib/dommy/form_data.rb +40 -6
- data/lib/dommy/form_entry_list.rb +106 -137
- data/lib/dommy/fragment.rb +11 -10
- data/lib/dommy/history.rb +247 -90
- data/lib/dommy/html_canvas_element.rb +15 -35
- data/lib/dommy/html_collection.rb +49 -30
- data/lib/dommy/html_elements/base.rb +111 -69
- data/lib/dommy/html_elements/embedded.rb +349 -105
- data/lib/dommy/html_elements/forms/form.rb +407 -368
- data/lib/dommy/html_elements/forms/input.rb +333 -174
- data/lib/dommy/html_elements/forms/select.rb +90 -74
- data/lib/dommy/html_elements/interactive.rb +274 -64
- data/lib/dommy/html_elements/media.rb +71 -30
- data/lib/dommy/html_elements/metadata.rb +215 -71
- data/lib/dommy/html_elements/miscellaneous.rb +58 -14
- data/lib/dommy/html_elements/tables.rb +7 -9
- data/lib/dommy/html_elements.rb +23 -1
- data/lib/dommy/hyperlink_utils.rb +15 -4
- data/lib/dommy/interaction/drag_and_drop.rb +187 -0
- data/lib/dommy/interaction/event_synthesis.rb +182 -24
- data/lib/dommy/interaction/field_interactor.rb +17 -5
- data/lib/dommy/interaction/form_submission.rb +10 -4
- data/lib/dommy/interaction/key_sender.rb +101 -16
- data/lib/dommy/interaction.rb +1 -0
- data/lib/dommy/internal/accessibility_tree.rb +15 -3
- data/lib/dommy/internal/accessible_description.rb +1 -1
- data/lib/dommy/internal/accessible_name.rb +10 -17
- data/lib/dommy/internal/aria_role.rb +1 -1
- data/lib/dommy/internal/autofill.rb +2 -2
- data/lib/dommy/internal/backend_prefilter.rb +1 -5
- data/lib/dommy/internal/ce_reactions.rb +285 -0
- data/lib/dommy/internal/ce_reactions_table.rb +296 -0
- data/lib/dommy/internal/child_list.rb +7 -43
- data/lib/dommy/internal/child_navigable.rb +155 -0
- data/lib/dommy/internal/child_node.rb +44 -41
- data/lib/dommy/internal/close_watcher_manager.rb +238 -0
- data/lib/dommy/internal/complex_match.rb +3 -1
- data/lib/dommy/internal/constraint_validation.rb +130 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
- data/lib/dommy/internal/css/cascade.rb +7 -7
- data/lib/dommy/internal/css/cascaded_declarations.rb +4 -1
- data/lib/dommy/internal/css/computed_style_builder.rb +9 -3
- data/lib/dommy/internal/css/custom_properties.rb +41 -29
- data/lib/dommy/internal/css/parser.rb +0 -22
- data/lib/dommy/internal/css/renderability.rb +6 -0
- data/lib/dommy/internal/css/rule_index.rb +149 -98
- data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
- data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
- data/lib/dommy/internal/css/style_cache.rb +7 -4
- data/lib/dommy/internal/css/ua_stylesheet.rb +34 -0
- data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
- data/lib/dommy/internal/document_generations.rb +55 -2
- data/lib/dommy/internal/document_interaction_state.rb +293 -9
- data/lib/dommy/internal/dom_matching.rb +2 -2
- data/lib/dommy/internal/element_aria.rb +25 -19
- data/lib/dommy/internal/element_popover.rb +226 -25
- data/lib/dommy/internal/element_shadow.rb +97 -26
- data/lib/dommy/internal/element_state.rb +98 -5
- data/lib/dommy/internal/element_top_layer.rb +15 -1
- data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
- data/lib/dommy/internal/event_handler_tables.rb +241 -0
- data/lib/dommy/internal/event_handlers.rb +149 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/focusability.rb +267 -0
- data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
- data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +34 -23
- data/lib/dommy/internal/html_serialization.rb +292 -0
- data/lib/dommy/internal/idl_reflection.rb +202 -0
- data/lib/dommy/internal/idl_reflection_table.rb +589 -0
- data/lib/dommy/internal/infra.rb +9 -0
- data/lib/dommy/internal/input_type.rb +76 -18
- data/lib/dommy/internal/insertion_point.rb +27 -3
- data/lib/dommy/internal/js_number.rb +60 -0
- data/lib/dommy/internal/light_dismiss.rb +178 -0
- data/lib/dommy/internal/mime_type.rb +8 -0
- data/lib/dommy/internal/mutation_coordinator.rb +94 -62
- data/lib/dommy/internal/node_adopter.rb +52 -4
- data/lib/dommy/internal/node_factory.rb +111 -7
- data/lib/dommy/internal/node_identity.rb +1 -1
- data/lib/dommy/internal/node_traversal.rb +22 -4
- data/lib/dommy/internal/node_wrapper_cache.rb +6 -38
- data/lib/dommy/internal/observer_options.rb +24 -14
- data/lib/dommy/internal/origin.rb +86 -0
- data/lib/dommy/internal/parent_node.rb +10 -8
- data/lib/dommy/internal/popover_invoker_element.rb +67 -0
- data/lib/dommy/internal/popover_stack.rb +120 -0
- data/lib/dommy/internal/post_insertion_steps.rb +140 -71
- data/lib/dommy/internal/reflected_attributes.rb +128 -18
- data/lib/dommy/internal/rendered_text/collector.rb +1 -5
- data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
- data/lib/dommy/internal/selector_arguments.rb +2 -1
- data/lib/dommy/internal/selector_match.rb +1 -1
- data/lib/dommy/internal/selector_matcher.rb +11 -6
- data/lib/dommy/internal/selector_parser.rb +14 -1
- data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
- data/lib/dommy/internal/shadow_root_registry.rb +7 -0
- data/lib/dommy/internal/template_content_registry.rb +7 -1
- data/lib/dommy/internal/text_selection.rb +185 -32
- data/lib/dommy/internal/toggle_events.rb +6 -4
- data/lib/dommy/internal/unsafe_html.rb +107 -0
- data/lib/dommy/internal/url_parser.rb +84 -8
- data/lib/dommy/internal/user_activation.rb +192 -0
- data/lib/dommy/internal/web_idl.rb +149 -16
- data/lib/dommy/internal/window_constructors.rb +26 -6
- data/lib/dommy/internal/xml_serialization.rb +3 -3
- data/lib/dommy/js/constructor_resolver.rb +18 -0
- data/lib/dommy/js/custom_element_bridge.rb +57 -82
- data/lib/dommy/js/dom_interfaces.rb +50 -15
- data/lib/dommy/js/host_bridge.rb +189 -40
- data/lib/dommy/js/host_runtime.js +1897 -415
- data/lib/dommy/js/marshaller.rb +13 -4
- data/lib/dommy/js/module_loader.rb +37 -6
- data/lib/dommy/js/module_preload.rb +107 -29
- data/lib/dommy/js/script_boot.rb +197 -115
- data/lib/dommy/js/serialized_record.rb +32 -0
- data/lib/dommy/js/webidl_event_handlers.js +45 -0
- data/lib/dommy/js/webidl_members.js +196 -0
- data/lib/dommy/js/webidl_signatures.js +2055 -0
- data/lib/dommy/js/webidl_tables.js +47 -100
- data/lib/dommy/js/window_wiring.rb +9 -5
- data/lib/dommy/location.rb +129 -76
- data/lib/dommy/media_query_list.rb +4 -13
- data/lib/dommy/message_channel.rb +137 -64
- data/lib/dommy/mutation_observer.rb +2 -0
- data/lib/dommy/navigation.rb +59 -9
- data/lib/dommy/navigator.rb +131 -5
- data/lib/dommy/node.rb +19 -11
- data/lib/dommy/notification.rb +9 -1
- data/lib/dommy/promise.rb +8 -0
- data/lib/dommy/range.rb +5 -34
- data/lib/dommy/resources.rb +38 -3
- data/lib/dommy/scheduler.rb +176 -48
- data/lib/dommy/shadow_root.rb +117 -19
- data/lib/dommy/storage.rb +135 -25
- data/lib/dommy/structured_data.rb +45 -0
- data/lib/dommy/style_declaration.rb +16 -0
- data/lib/dommy/text_codec.rb +2 -14
- data/lib/dommy/tree_walker.rb +10 -18
- data/lib/dommy/url.rb +11 -4
- data/lib/dommy/validity_state.rb +109 -107
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +5 -28
- data/lib/dommy/window.rb +811 -99
- data/lib/dommy/worker.rb +4 -28
- data/lib/dommy/xml_http_request.rb +32 -45
- data/lib/dommy.rb +35 -1
- metadata +35 -4
- data/lib/dommy/internal/cookie_jar.rb +0 -29
data/lib/dommy/browser.rb
CHANGED
|
@@ -71,21 +71,50 @@ module Dommy
|
|
|
71
71
|
@console = []
|
|
72
72
|
@disposed = false
|
|
73
73
|
@pending_navigations = []
|
|
74
|
+
@popups = []
|
|
74
75
|
@runtime = nil
|
|
76
|
+
# One browsing session: every window it shows (and their frames) shares
|
|
77
|
+
# localStorage per origin and sessionStorage per origin.
|
|
78
|
+
@storage_provider = StorageProvider.new
|
|
79
|
+
# One cookie store for the session: document.cookie, cookieStore, and
|
|
80
|
+
# the Cookie / Set-Cookie of every request it sends through resources.
|
|
81
|
+
@cookie_jar = CookieJar.new
|
|
82
|
+
@before_unload_handler = nil
|
|
75
83
|
|
|
76
84
|
@window = Dommy.parse(html)
|
|
77
85
|
@window.location.__internal_set_url__(url) if url
|
|
78
|
-
|
|
86
|
+
@window.storage_provider = @storage_provider
|
|
87
|
+
@window.cookie_jar = @cookie_jar
|
|
79
88
|
|
|
80
89
|
if navigable
|
|
81
|
-
@fetcher =
|
|
90
|
+
@fetcher = navigation_fetcher
|
|
82
91
|
@history = Navigation::JointHistory.new
|
|
83
92
|
@window.navigation_delegate = self
|
|
84
93
|
@history.push(current_url, window: @window, windex: @window.history.__internal_index__)
|
|
94
|
+
install_history_sync(@window)
|
|
85
95
|
end
|
|
96
|
+
install_runtime(@window)
|
|
86
97
|
check_js_errors!
|
|
87
98
|
end
|
|
88
99
|
|
|
100
|
+
# The provider of this browser's Web Storage areas (shared by every window
|
|
101
|
+
# it shows); see Dommy::StorageProvider.
|
|
102
|
+
attr_reader :storage_provider
|
|
103
|
+
|
|
104
|
+
# This browser's cookies (a Dommy::CookieJar): the one store its pages'
|
|
105
|
+
# document.cookie and cookieStore use and its requests send and fill.
|
|
106
|
+
attr_reader :cookie_jar
|
|
107
|
+
|
|
108
|
+
# Install a handler for a page that asks to confirm leaving it (a canceled
|
|
109
|
+
# `beforeunload`, or one whose returnValue is set): called with the window
|
|
110
|
+
# and the BeforeUnloadEvent, its truthy answer lets the navigation proceed.
|
|
111
|
+
# Without one the navigation always proceeds — a headless page has no sticky
|
|
112
|
+
# user activation, so a browser would not prompt either.
|
|
113
|
+
def on_before_unload(&block)
|
|
114
|
+
@before_unload_handler = block
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
|
|
89
118
|
def document = @window.document
|
|
90
119
|
|
|
91
120
|
# Current document HTML (serialized).
|
|
@@ -117,8 +146,8 @@ module Dommy
|
|
|
117
146
|
# Move back / forward one entry in the joint history. A same-document target
|
|
118
147
|
# (the entry's window is still live) traverses in place (popstate); a
|
|
119
148
|
# document-boundary target is re-fetched (no bfcache — D2).
|
|
120
|
-
def back =
|
|
121
|
-
def forward =
|
|
149
|
+
def back = traverse_now(-1)
|
|
150
|
+
def forward = traverse_now(1)
|
|
122
151
|
|
|
123
152
|
# --- NavigationDelegate port (see Dommy::Navigation) ---
|
|
124
153
|
|
|
@@ -138,7 +167,7 @@ module Dommy
|
|
|
138
167
|
navigate_frame(frame,
|
|
139
168
|
{method: method, url: url, params: params, body: body, enctype: enctype, headers: headers},
|
|
140
169
|
resolve_against_current(url.to_s))
|
|
141
|
-
@window.scheduler.queue_microtask(proc { frame.
|
|
170
|
+
@window.scheduler.queue_microtask(proc { frame.__internal_run_iframe_load_event_steps__ })
|
|
142
171
|
return nil
|
|
143
172
|
end
|
|
144
173
|
|
|
@@ -164,6 +193,109 @@ module Dommy
|
|
|
164
193
|
end
|
|
165
194
|
|
|
166
195
|
def traverse(_delta) = nil
|
|
196
|
+
|
|
197
|
+
def history_length = @browser.history_length
|
|
198
|
+
|
|
199
|
+
# The frames of the document loaded into this frame load the same way.
|
|
200
|
+
def load_frame(frame, **nav) = @browser.load_frame(frame, **nav)
|
|
201
|
+
|
|
202
|
+
# A popup opened from inside the frame is the browser's too.
|
|
203
|
+
def open_window(**opts) = @browser.open_window(**opts)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The navigation delegate of an auxiliary window this browser opened
|
|
207
|
+
# (window.open): its navigations load into it, keeping the Window the
|
|
208
|
+
# opener holds (Dommy has no WindowProxy, so the document is swapped
|
|
209
|
+
# under it), and its close() closes it.
|
|
210
|
+
class PopupDelegate
|
|
211
|
+
def initialize(browser, popup)
|
|
212
|
+
@browser = browser
|
|
213
|
+
@popup = popup
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, target: nil, headers: {}, replace: false)
|
|
217
|
+
@browser.__internal_load_popup__(@popup, {url: url, method: method, body: body, params: params,
|
|
218
|
+
enctype: enctype, headers: headers})
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def traverse(_delta) = nil
|
|
222
|
+
|
|
223
|
+
def close_window = @browser.__internal_close_popup__(@popup)
|
|
224
|
+
|
|
225
|
+
def find_window(name) = @browser.find_window(name)
|
|
226
|
+
|
|
227
|
+
def load_frame(frame, **nav) = @browser.load_frame(frame, **nav)
|
|
228
|
+
|
|
229
|
+
def open_window(**opts) = @browser.open_window(**opts)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The auxiliary windows the page opened with window.open and has not
|
|
233
|
+
# closed, oldest first.
|
|
234
|
+
def popups = @popups.dup
|
|
235
|
+
|
|
236
|
+
# NavigationDelegate `open_window`: window.open asked for a new top-level
|
|
237
|
+
# browsing context. It is created at once with the initial about:blank
|
|
238
|
+
# (whose origin is the opener's) and navigated to `url` from a task; it
|
|
239
|
+
# shares this browser's cookies and localStorage, and starts its
|
|
240
|
+
# sessionStorage as a copy of this one's. Window#window_open sets its
|
|
241
|
+
# opener and name.
|
|
242
|
+
def open_window(url:, target:, features: "")
|
|
243
|
+
return nil if @disposed
|
|
244
|
+
|
|
245
|
+
popup = Dommy.parse(Internal::ChildNavigable::BLANK_HTML)
|
|
246
|
+
popup.location.__internal_set_url__("about:blank")
|
|
247
|
+
popup.__internal_initial_about_blank__ = true
|
|
248
|
+
popup.storage_provider = @storage_provider.new_session
|
|
249
|
+
popup.cookie_jar = @cookie_jar
|
|
250
|
+
popup.navigation_delegate = PopupDelegate.new(self, popup)
|
|
251
|
+
@popups << popup
|
|
252
|
+
unless Internal::ChildNavigable.matches_about_blank?(url)
|
|
253
|
+
@window.scheduler.set_timeout(proc { __internal_load_popup__(popup, {url: url, method: "GET"}) }, 0)
|
|
254
|
+
end
|
|
255
|
+
popup
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Load a navigation into a popup: fetch it, give the popup's Window the
|
|
259
|
+
# document, and fire its load. Nothing happens when nothing serves it.
|
|
260
|
+
def __internal_load_popup__(popup, nav)
|
|
261
|
+
return nil if @disposed || @resources.nil? || !@popups.include?(popup)
|
|
262
|
+
|
|
263
|
+
url = nav[:url].to_s
|
|
264
|
+
@fetcher ||= navigation_fetcher
|
|
265
|
+
response, final_url = begin
|
|
266
|
+
@fetcher.request(method: nav[:method] || "GET", url: url, params: nav[:params],
|
|
267
|
+
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {})
|
|
268
|
+
rescue StandardError
|
|
269
|
+
[nil, url]
|
|
270
|
+
end
|
|
271
|
+
return nil unless response && document_response?(response)
|
|
272
|
+
|
|
273
|
+
loaded = Internal::ChildNavigable.window_for_response(response.body, response_header(response, "content-type"), final_url)
|
|
274
|
+
popup.__internal_adopt_document_of__(loaded)
|
|
275
|
+
popup.navigation_delegate = PopupDelegate.new(self, popup)
|
|
276
|
+
document = popup.document
|
|
277
|
+
document.__internal_update_readiness__("loading")
|
|
278
|
+
document.__internal_finish_loading__
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# NavigationDelegate `find_window`: the top-level window (this browser's
|
|
283
|
+
# page or a popup) or a frame in one, whose target name is `name`.
|
|
284
|
+
def find_window(name)
|
|
285
|
+
([@window] + @popups).each do |top|
|
|
286
|
+
return top if top.name == name
|
|
287
|
+
|
|
288
|
+
found = top.__internal_find_descendant_window__(name)
|
|
289
|
+
return found if found
|
|
290
|
+
end
|
|
291
|
+
nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# A popup's close(): it leaves the browser, closed.
|
|
295
|
+
def __internal_close_popup__(popup)
|
|
296
|
+
@popups.delete(popup)
|
|
297
|
+
popup.__internal_discard__
|
|
298
|
+
nil
|
|
167
299
|
end
|
|
168
300
|
|
|
169
301
|
def frame_navigation_delegate(frame) = FrameNavigationDelegate.new(self, frame)
|
|
@@ -179,26 +311,49 @@ module Dommy
|
|
|
179
311
|
nav[:url].to_s
|
|
180
312
|
end
|
|
181
313
|
navigate_frame(frame, nav, resolved)
|
|
182
|
-
frame.
|
|
314
|
+
frame.__internal_run_iframe_load_event_steps__
|
|
183
315
|
nil
|
|
184
316
|
end
|
|
185
317
|
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
318
|
+
# NavigationDelegate `load_frame`: an iframe's child navigable is navigating
|
|
319
|
+
# to `url` (its `src`, or a navigation from inside it) — fetch it through
|
|
320
|
+
# this browser's resources and answer the document's Window, or nil when
|
|
321
|
+
# nothing serves it (the frame then keeps its document). The frame installs
|
|
322
|
+
# it and fires its own `load`.
|
|
323
|
+
def load_frame(frame, url:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, **)
|
|
324
|
+
return nil if @disposed || @resources.nil?
|
|
325
|
+
|
|
326
|
+
frame_window_for(frame, {url: url, method: method, body: body, params: params, enctype: enctype, headers: headers}, url.to_s)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# A history traversal by `delta` that the PAGE asked for (`history.go(n)`
|
|
330
|
+
# past its own document's entries): like a navigation it is a task, so it
|
|
331
|
+
# is recorded and performed at the next drain boundary.
|
|
189
332
|
def traverse(delta)
|
|
190
|
-
|
|
333
|
+
@pending_navigations << {traverse: delta.to_i} if @navigable
|
|
334
|
+
nil
|
|
335
|
+
end
|
|
191
336
|
|
|
192
|
-
|
|
193
|
-
|
|
337
|
+
# `history.length`: the joint session history's size (nil when this
|
|
338
|
+
# browser keeps none).
|
|
339
|
+
def history_length = (@history.length if @navigable)
|
|
194
340
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
341
|
+
# `window.stop()`: drop the navigations the page asked for and that have not
|
|
342
|
+
# been performed yet.
|
|
343
|
+
def stop
|
|
344
|
+
@pending_navigations.clear
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# A Ruby-initiated traversal (back / forward) runs immediately: a
|
|
349
|
+
# same-document target (its window is still live) traverses in place
|
|
350
|
+
# (popstate); a document-boundary target is re-fetched.
|
|
351
|
+
def traverse_now(delta)
|
|
352
|
+
return self unless @navigable
|
|
353
|
+
|
|
354
|
+
perform_traversal!(delta)
|
|
355
|
+
@runtime.drain_microtasks
|
|
356
|
+
check_js_errors!
|
|
202
357
|
self
|
|
203
358
|
end
|
|
204
359
|
|
|
@@ -221,6 +376,7 @@ module Dommy
|
|
|
221
376
|
# `setTimeout(300)` — use `advance_time(300)` for debounce/throttle.
|
|
222
377
|
def settle
|
|
223
378
|
@runtime.settle
|
|
379
|
+
drive_popups(0)
|
|
224
380
|
flush_navigation!
|
|
225
381
|
check_js_errors!
|
|
226
382
|
self
|
|
@@ -229,6 +385,7 @@ module Dommy
|
|
|
229
385
|
# Advance virtual time by `ms`, running timers that come due, then settle.
|
|
230
386
|
def advance_time(ms)
|
|
231
387
|
@window.scheduler.advance_time(ms)
|
|
388
|
+
drive_popups(ms)
|
|
232
389
|
@runtime.drain_microtasks
|
|
233
390
|
flush_navigation!
|
|
234
391
|
check_js_errors!
|
|
@@ -240,6 +397,7 @@ module Dommy
|
|
|
240
397
|
# land before the next line, then enforce strict mode.
|
|
241
398
|
def after_interaction
|
|
242
399
|
@runtime.drain_microtasks
|
|
400
|
+
drive_popups(0)
|
|
243
401
|
flush_navigation!
|
|
244
402
|
check_js_errors!
|
|
245
403
|
end
|
|
@@ -319,7 +477,10 @@ module Dommy
|
|
|
319
477
|
# Opt-in WPT scaffolding (common/sab.js derives SharedArrayBuffer through
|
|
320
478
|
# WebAssembly.Memory); off by default so real pages don't see the shim.
|
|
321
479
|
runtime.install_wasm_memory_shim if @wasm_memory_shim && runtime.respond_to?(:install_wasm_memory_shim)
|
|
322
|
-
|
|
480
|
+
if @resources
|
|
481
|
+
window.globals["__fetch_handler__"] =
|
|
482
|
+
Resources::FetchHandler.new(@resources, cookie_jar: @cookie_jar, origin: -> { window.origin })
|
|
483
|
+
end
|
|
323
484
|
@runtime = runtime
|
|
324
485
|
doc = window.document
|
|
325
486
|
# An `on*` attribute that arrived after boot (a cloned template, an
|
|
@@ -327,7 +488,7 @@ module Dommy
|
|
|
327
488
|
# Installed whenever a runtime is attached — an embedder that drives script
|
|
328
489
|
# boot itself (`execute_scripts: false`) still needs inline handlers wired.
|
|
329
490
|
doc.inline_handler_wirer = lambda do
|
|
330
|
-
Js::ScriptBoot.wire_inline_handlers(runtime, on_error: ->(e) { @error_log.record(e) })
|
|
491
|
+
Js::ScriptBoot.wire_inline_handlers(runtime, document: doc, on_error: ->(e) { @error_log.record(e) })
|
|
331
492
|
end
|
|
332
493
|
return unless @execute_scripts
|
|
333
494
|
|
|
@@ -348,6 +509,16 @@ module Dommy
|
|
|
348
509
|
runtime.settle if @settle_after_boot
|
|
349
510
|
end
|
|
350
511
|
|
|
512
|
+
# A popup has no realm of its own; the tasks queued on its window (a
|
|
513
|
+
# close(), its timers) run as the browser's time moves.
|
|
514
|
+
def drive_popups(ms)
|
|
515
|
+
@popups.dup.each { |popup| popup.scheduler.advance_time(ms) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def navigation_fetcher
|
|
519
|
+
Navigation::Fetcher.new(@resources, same_origin: @same_origin, cookie_jar: @cookie_jar)
|
|
520
|
+
end
|
|
521
|
+
|
|
351
522
|
# Route an unhandled promise rejection through the page's own
|
|
352
523
|
# `unhandledrejection` handling first. The engine decides WHEN a rejection
|
|
353
524
|
# counts as unhandled (see Window#__internal_report_rejection__).
|
|
@@ -371,22 +542,31 @@ module Dommy
|
|
|
371
542
|
frame = resolve_target_frame(nav[:target])
|
|
372
543
|
return navigate_frame(frame, nav, resolved) if frame
|
|
373
544
|
|
|
545
|
+
# The outgoing page may ask to confirm leaving (beforeunload).
|
|
546
|
+
return unless unloading_allowed?(@window)
|
|
547
|
+
|
|
374
548
|
response, final_url = @fetcher.request(
|
|
375
549
|
method: nav[:method] || "GET", url: resolved, params: nav[:params],
|
|
376
550
|
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {}
|
|
377
551
|
)
|
|
378
552
|
return unless response&.success? && document_response?(response)
|
|
379
553
|
|
|
554
|
+
old_window = @window
|
|
555
|
+
referrer = referrer_for(nav[:source], old_window.location.href, final_url)
|
|
380
556
|
# Fire the outgoing document's unload sequence while its realm is still
|
|
381
557
|
# alive, then surface any of its errors before the realm is torn down.
|
|
382
|
-
fire_unload(
|
|
558
|
+
fire_unload(old_window)
|
|
383
559
|
check_js_errors!
|
|
384
560
|
|
|
385
|
-
new_window = Dommy.parse(response.body)
|
|
561
|
+
new_window = Dommy.parse(Encodings.decode_document(response.body, response_header(response, "content-type")))
|
|
386
562
|
new_window.location.__internal_set_url__(final_url)
|
|
563
|
+
new_window.document.__internal_set_last_modified__(response_header(response, "last-modified"))
|
|
564
|
+
new_window.document.__internal_referrer__ = referrer if referrer
|
|
387
565
|
new_window.navigation_delegate = self
|
|
566
|
+
new_window.storage_provider = @storage_provider
|
|
567
|
+
new_window.cookie_jar = @cookie_jar
|
|
568
|
+
old_window.__internal_discard__
|
|
388
569
|
@window = new_window
|
|
389
|
-
install_runtime(new_window)
|
|
390
570
|
|
|
391
571
|
windex = new_window.history.__internal_index__
|
|
392
572
|
if rebind || nav[:replace]
|
|
@@ -394,10 +574,82 @@ module Dommy
|
|
|
394
574
|
else
|
|
395
575
|
@history.push(final_url, window: new_window, windex: windex)
|
|
396
576
|
end
|
|
577
|
+
# The joint history knows the new page before its scripts boot, so a
|
|
578
|
+
# pushState / history.length during boot sees the right session.
|
|
579
|
+
install_history_sync(new_window)
|
|
580
|
+
install_runtime(new_window)
|
|
397
581
|
|
|
398
582
|
follow_meta_refresh!(refresh_depth)
|
|
399
583
|
end
|
|
400
584
|
|
|
585
|
+
# Fire `beforeunload` at the outgoing window; a page that cancels it (or
|
|
586
|
+
# sets returnValue) is asked about through the on_before_unload handler, if
|
|
587
|
+
# one is installed.
|
|
588
|
+
def unloading_allowed?(window)
|
|
589
|
+
event = BeforeUnloadEvent.new("beforeunload", "cancelable" => true).__internal_mark_trusted__
|
|
590
|
+
not_canceled = window.dispatch_event(event)
|
|
591
|
+
return true if not_canceled && event.return_value.to_s.empty?
|
|
592
|
+
return true unless @before_unload_handler
|
|
593
|
+
|
|
594
|
+
@before_unload_handler.call(window, event) ? true : false
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# The `document.referrer` a navigation hands the new document: the page
|
|
598
|
+
# that started it (links, forms, script), under the default
|
|
599
|
+
# strict-origin-when-cross-origin policy — the full URL (without fragment
|
|
600
|
+
# or credentials) within an origin, only the origin across origins, nothing
|
|
601
|
+
# on an https -> http downgrade. Ruby-initiated visits and traversals carry
|
|
602
|
+
# none.
|
|
603
|
+
def referrer_for(source, from_url, to_url)
|
|
604
|
+
return nil unless %i[link form location window_open].include?(source)
|
|
605
|
+
|
|
606
|
+
from = URL.new(from_url.to_s)
|
|
607
|
+
to = URL.new(to_url.to_s)
|
|
608
|
+
return nil unless %w[http: https:].include?(from.protocol)
|
|
609
|
+
return "" if from.protocol == "https:" && to.protocol == "http:"
|
|
610
|
+
return "#{from.origin}/" unless from.origin == to.origin
|
|
611
|
+
|
|
612
|
+
from.hash = ""
|
|
613
|
+
from.username = ""
|
|
614
|
+
from.password = ""
|
|
615
|
+
from.href
|
|
616
|
+
rescue StandardError
|
|
617
|
+
nil
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# A traversal by `delta` of the joint history: within the live window's own
|
|
621
|
+
# entries it moves that window's history (popstate); across a document
|
|
622
|
+
# boundary the target entry's URL is re-fetched (no bfcache).
|
|
623
|
+
def perform_traversal!(delta)
|
|
624
|
+
entry = @history.go(delta)
|
|
625
|
+
return unless entry
|
|
626
|
+
|
|
627
|
+
if entry.window && entry.window.equal?(@window)
|
|
628
|
+
@window.history.__internal_go_to__(entry.windex)
|
|
629
|
+
else
|
|
630
|
+
perform_navigation!({url: entry.url, method: "GET", source: :traverse}, rebind: true)
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Mirror the page's same-document history changes (pushState, fragment
|
|
635
|
+
# navigations, its own back/forward within the document) into the joint
|
|
636
|
+
# history, so Browser#back and history.length see them. Guarded against a
|
|
637
|
+
# navigated-away window.
|
|
638
|
+
def install_history_sync(window)
|
|
639
|
+
window.history.__internal_on_change__ = lambda do |kind, url|
|
|
640
|
+
next unless window.equal?(@window)
|
|
641
|
+
|
|
642
|
+
case kind
|
|
643
|
+
when :push
|
|
644
|
+
@history.push(url, window: window, windex: window.history.__internal_index__)
|
|
645
|
+
when :replace
|
|
646
|
+
@history.rebind_current(url: url, window: window, windex: window.history.__internal_index__)
|
|
647
|
+
when :traverse
|
|
648
|
+
@history.sync_to(window, window.history.__internal_index__)
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
401
653
|
# The reserved browsing-context keywords; anything else names an iframe.
|
|
402
654
|
RESERVED_TARGETS = %w[_self _top _parent _blank].freeze
|
|
403
655
|
|
|
@@ -417,32 +669,43 @@ module Dommy
|
|
|
417
669
|
# it, install the response document as the frame's content, and fire the
|
|
418
670
|
# frame's `load`. The top-level window and joint history are left alone.
|
|
419
671
|
def navigate_frame(frame, nav, resolved_url)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return unless response&.success?
|
|
672
|
+
# The frame makes about:, data: and blob: documents itself and asks
|
|
673
|
+
# this browser (#load_frame) for the rest.
|
|
674
|
+
sub_window = frame.__internal_child_document_for__(resolved_url, nil, nav.except(:url))
|
|
675
|
+
return nil unless sub_window
|
|
425
676
|
|
|
426
|
-
sub_window = frame_document_for(response)
|
|
427
|
-
sub_window.location.__internal_set_url__(final_url)
|
|
428
677
|
# A navigation from inside the loaded frame also stays in that frame.
|
|
429
|
-
sub_window.navigation_delegate = frame_navigation_delegate(frame)
|
|
430
|
-
# A nested realm needs the seeded constructors to run the response's
|
|
431
|
-
# scripts; a runtime that cannot expose them simply runs without them.
|
|
432
|
-
@runtime.expose_constructors_on(sub_window) if @runtime.respond_to?(:expose_constructors_on)
|
|
678
|
+
sub_window.navigation_delegate = frame_navigation_delegate(frame) if sub_window.navigation_delegate.is_a?(Navigation::NullDelegate)
|
|
433
679
|
frame.__internal_set_content_document__(sub_window.document)
|
|
434
680
|
nil
|
|
435
681
|
end
|
|
436
682
|
|
|
437
|
-
#
|
|
438
|
-
#
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
683
|
+
# Fetch a frame navigation and build the Window of the document it
|
|
684
|
+
# answers. nil when nothing (successful) serves the URL.
|
|
685
|
+
def frame_window_for(frame, nav, resolved_url)
|
|
686
|
+
@fetcher ||= navigation_fetcher
|
|
687
|
+
response, final_url = begin
|
|
688
|
+
@fetcher.request(
|
|
689
|
+
method: nav[:method] || "GET", url: resolved_url, params: nav[:params],
|
|
690
|
+
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {}
|
|
691
|
+
)
|
|
692
|
+
rescue StandardError
|
|
693
|
+
# A resources adapter that cannot serve the request at all fails the
|
|
694
|
+
# navigation like a network error.
|
|
695
|
+
[nil, resolved_url]
|
|
696
|
+
end
|
|
697
|
+
# Nothing answered: a network error, which shows an error page (and
|
|
698
|
+
# still fires the frame's load). A response of any status is a document.
|
|
699
|
+
return Internal::ChildNavigable.error_window(final_url || resolved_url) unless response
|
|
442
700
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
701
|
+
sub_window = Internal::ChildNavigable.window_for_response(
|
|
702
|
+
response.body, response_header(response, "content-type"), final_url
|
|
703
|
+
)
|
|
704
|
+
sub_window.document.__internal_set_last_modified__(response_header(response, "last-modified"))
|
|
705
|
+
# A nested realm needs the seeded constructors to run the response's
|
|
706
|
+
# scripts; a runtime that cannot expose them simply runs without them.
|
|
707
|
+
@runtime.expose_constructors_on(sub_window) if @runtime.respond_to?(:expose_constructors_on)
|
|
708
|
+
sub_window
|
|
446
709
|
end
|
|
447
710
|
|
|
448
711
|
# If the freshly loaded document asks for an immediate `<meta http-equiv=
|
|
@@ -489,24 +752,36 @@ module Dommy
|
|
|
489
752
|
|
|
490
753
|
navs = @pending_navigations
|
|
491
754
|
@pending_navigations = []
|
|
492
|
-
navs.each
|
|
755
|
+
navs.each do |nav|
|
|
756
|
+
nav.key?(:traverse) ? perform_traversal!(nav[:traverse]) : perform_navigation!(nav)
|
|
757
|
+
end
|
|
493
758
|
end
|
|
494
759
|
|
|
760
|
+
# Unload the outgoing document: `pagehide` (a PageTransitionEvent, persisted
|
|
761
|
+
# false), then `unload` — both trusted and targeted at the document.
|
|
495
762
|
def fire_unload(window)
|
|
496
|
-
window.
|
|
497
|
-
window.dispatch_event(
|
|
763
|
+
document = window.document
|
|
764
|
+
window.dispatch_event(document.__internal_page_transition_event__("pagehide"))
|
|
765
|
+
unload = Dommy::Event.new("unload")
|
|
766
|
+
unload.__internal_set_target__(document)
|
|
767
|
+
window.dispatch_event(unload.__internal_mark_trusted__)
|
|
498
768
|
end
|
|
499
769
|
|
|
500
770
|
# Only HTML/XML responses replace the document; other content types (a JSON
|
|
501
771
|
# API hit, an image) leave the current page. A response with no Content-Type
|
|
502
772
|
# is treated as a document (fixtures commonly omit it).
|
|
503
773
|
def document_response?(response)
|
|
504
|
-
|
|
505
|
-
key = headers.keys.find { |k| k.to_s.casecmp?("content-type") }
|
|
506
|
-
content_type = key ? headers[key].to_s.downcase : ""
|
|
774
|
+
content_type = response_header(response, "content-type").to_s.downcase
|
|
507
775
|
content_type.empty? || content_type.include?("html") || content_type.include?("xml")
|
|
508
776
|
end
|
|
509
777
|
|
|
778
|
+
# A response header's value by case-insensitive name, or nil.
|
|
779
|
+
def response_header(response, name)
|
|
780
|
+
headers = response.headers || {}
|
|
781
|
+
key = headers.keys.find { |k| k.to_s.casecmp?(name) }
|
|
782
|
+
key && headers[key]
|
|
783
|
+
end
|
|
784
|
+
|
|
510
785
|
def submit_button?(button)
|
|
511
786
|
if button.tag_name == "BUTTON"
|
|
512
787
|
button.type == "submit"
|
data/lib/dommy/character_data.rb
CHANGED
|
@@ -89,9 +89,10 @@ module Dommy
|
|
|
89
89
|
@__node__.content
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Assigning `data`, `nodeValue` or `textContent` is "replace data" over
|
|
93
|
+
# the whole node, so a live range boundary inside it clamps to the start
|
|
94
|
+
# rather than dangling past the end.
|
|
92
95
|
def data=(value)
|
|
93
|
-
# Assigning `data` is "replace data" over the whole node, so a live range
|
|
94
|
-
# boundary inside it clamps to the start rather than dangling past the end.
|
|
95
96
|
old_length = utf16_length(@__node__.content)
|
|
96
97
|
write_data(value)
|
|
97
98
|
@document.__internal_ranges_replaced_data__(self, 0, old_length, utf16_length(@__node__.content))
|
|
@@ -102,7 +103,7 @@ module Dommy
|
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def node_value=(value)
|
|
105
|
-
|
|
106
|
+
self.data = value
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def text_content
|
|
@@ -110,9 +111,16 @@ module Dommy
|
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def text_content=(value)
|
|
113
|
-
|
|
114
|
+
self.data = value
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Node.isConnected: whether the shadow-including root is a Document.
|
|
118
|
+
def is_connected?
|
|
119
|
+
Internal::NodeTraversal.connected?(@__node__, @document)
|
|
114
120
|
end
|
|
115
121
|
|
|
122
|
+
alias connected? is_connected?
|
|
123
|
+
|
|
116
124
|
def remove
|
|
117
125
|
@document.remove_node_with_notify(@__node__)
|
|
118
126
|
nil
|
|
@@ -178,29 +186,13 @@ module Dommy
|
|
|
178
186
|
def substring_data(offset, count)
|
|
179
187
|
s = @__node__.content
|
|
180
188
|
len = utf16_length(s)
|
|
181
|
-
o =
|
|
189
|
+
o = Internal::WebIDL.unsigned_long(offset)
|
|
182
190
|
raise DOMException::IndexSizeError, "offset out of bounds" if o > len
|
|
183
191
|
|
|
184
|
-
c = [
|
|
192
|
+
c = [Internal::WebIDL.unsigned_long(count), len - o].min
|
|
185
193
|
utf16_slice(s, o, c)
|
|
186
194
|
end
|
|
187
195
|
|
|
188
|
-
# ECMAScript ToUint32 — WebIDL `unsigned long` conversion for a data offset
|
|
189
|
-
# or count: ToNumber (a non-numeric string is NaN), truncate toward zero,
|
|
190
|
-
# then take modulo 2**32 (so -1 wraps to 4294967295, 0x100000000+2 to 2).
|
|
191
|
-
def to_uint32(value)
|
|
192
|
-
num =
|
|
193
|
-
case value
|
|
194
|
-
when Integer then value
|
|
195
|
-
when Numeric then value
|
|
196
|
-
when nil then 0 # JS null -> 0
|
|
197
|
-
else Float(value.to_s) rescue Float::NAN
|
|
198
|
-
end
|
|
199
|
-
return 0 unless num.respond_to?(:finite?) ? num.finite? : true
|
|
200
|
-
|
|
201
|
-
num.to_i % (2**32)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
196
|
def append_data(value)
|
|
205
197
|
write_data(@__node__.content + dom_string(value))
|
|
206
198
|
end
|
|
@@ -222,10 +214,10 @@ module Dommy
|
|
|
222
214
|
def replace_data(offset, count, value)
|
|
223
215
|
s = @__node__.content
|
|
224
216
|
len = utf16_length(s)
|
|
225
|
-
o =
|
|
217
|
+
o = Internal::WebIDL.unsigned_long(offset)
|
|
226
218
|
raise DOMException::IndexSizeError, "offset out of bounds" if o > len
|
|
227
219
|
|
|
228
|
-
c = [
|
|
220
|
+
c = [Internal::WebIDL.unsigned_long(count), len - o].min
|
|
229
221
|
inserted = dom_string(value)
|
|
230
222
|
write_data(utf16_slice(s, 0, o) + inserted + utf16_slice(s, o + c, len - (o + c)))
|
|
231
223
|
# Live ranges whose boundary sits in (or past) the replaced run follow it.
|
|
@@ -256,6 +248,8 @@ module Dommy
|
|
|
256
248
|
next_sibling
|
|
257
249
|
when "previousSibling"
|
|
258
250
|
previous_sibling
|
|
251
|
+
when "isConnected"
|
|
252
|
+
is_connected?
|
|
259
253
|
when "childNodes"
|
|
260
254
|
# CharacterData is a leaf node: childNodes is always an empty (but
|
|
261
255
|
# present and iterable) NodeList, and firstChild/lastChild are null.
|
|
@@ -293,7 +287,7 @@ module Dommy
|
|
|
293
287
|
def __js_set__(key, value)
|
|
294
288
|
case key
|
|
295
289
|
when "textContent", "data", "nodeValue"
|
|
296
|
-
|
|
290
|
+
self.data = value
|
|
297
291
|
end
|
|
298
292
|
|
|
299
293
|
nil
|
|
@@ -312,8 +306,7 @@ module Dommy
|
|
|
312
306
|
false
|
|
313
307
|
when "contains"
|
|
314
308
|
# A leaf node contains only itself (no descendants).
|
|
315
|
-
args[0].
|
|
316
|
-
args[0].__dommy_backend_node__ == @__node__
|
|
309
|
+
args[0].is_a?(Node) && args[0].__dommy_backend_node__ == @__node__
|
|
317
310
|
when "appendChild"
|
|
318
311
|
append_child(args[0])
|
|
319
312
|
when "insertBefore"
|