dommy 0.14.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/README.md +15 -1
- data/lib/dommy/attr.rb +56 -32
- data/lib/dommy/backend.rb +251 -208
- data/lib/dommy/blob.rb +24 -0
- data/lib/dommy/bridge/methods.rb +12 -2
- data/lib/dommy/bridge/values.rb +29 -2
- data/lib/dommy/bridge/wire_tags.rb +8 -4
- data/lib/dommy/browser.rb +328 -53
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +21 -28
- data/lib/dommy/class_list.rb +55 -23
- data/lib/dommy/cookie_jar.rb +408 -0
- data/lib/dommy/cookie_store.rb +104 -49
- data/lib/dommy/css.rb +37 -8
- data/lib/dommy/custom_elements.rb +360 -116
- data/lib/dommy/data_transfer.rb +22 -2
- data/lib/dommy/document.rb +1258 -346
- 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 +408 -569
- data/lib/dommy/element_internals.rb +215 -0
- data/lib/dommy/encodings.rb +10 -0
- data/lib/dommy/event.rb +382 -148
- data/lib/dommy/event_source.rb +8 -34
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/file_reader.rb +5 -29
- data/lib/dommy/form_data.rb +53 -16
- data/lib/dommy/form_entry_list.rb +107 -138
- data/lib/dommy/fragment.rb +14 -16
- data/lib/dommy/history.rb +247 -90
- data/lib/dommy/html_canvas_element.rb +15 -35
- data/lib/dommy/html_collection.rb +76 -50
- data/lib/dommy/html_elements/base.rb +377 -71
- data/lib/dommy/html_elements/embedded.rb +350 -106
- data/lib/dommy/html_elements/forms/form.rb +424 -383
- data/lib/dommy/html_elements/forms/input.rb +351 -189
- data/lib/dommy/html_elements/forms/select.rb +97 -81
- data/lib/dommy/html_elements/interactive.rb +285 -72
- data/lib/dommy/html_elements/media.rb +73 -32
- data/lib/dommy/html_elements/metadata.rb +226 -78
- data/lib/dommy/html_elements/miscellaneous.rb +58 -14
- data/lib/dommy/html_elements/tables.rb +20 -42
- data/lib/dommy/html_elements.rb +28 -3
- data/lib/dommy/hyperlink_utils.rb +19 -8
- 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 +18 -6
- data/lib/dommy/interaction/form_submission.rb +12 -6
- data/lib/dommy/interaction/key_sender.rb +102 -17
- data/lib/dommy/interaction.rb +1 -0
- data/lib/dommy/internal/accessibility_tree.rb +19 -7
- data/lib/dommy/internal/accessibility_visibility.rb +2 -2
- data/lib/dommy/internal/accessible_description.rb +3 -3
- data/lib/dommy/internal/accessible_name.rb +33 -45
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +27 -18
- data/lib/dommy/internal/aria_state.rb +7 -7
- data/lib/dommy/internal/autofill.rb +2 -2
- data/lib/dommy/internal/backend_prefilter.rb +21 -18
- 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 +39 -0
- 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 +129 -0
- 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 +14 -20
- data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
- data/lib/dommy/internal/css/custom_properties.rb +63 -80
- data/lib/dommy/internal/css/parser.rb +53 -63
- data/lib/dommy/internal/css/renderability.rb +6 -0
- data/lib/dommy/internal/css/rule_index.rb +170 -108
- 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 +9 -6
- data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
- data/lib/dommy/internal/directionality.rb +2 -2
- data/lib/dommy/internal/document_generations.rb +91 -18
- data/lib/dommy/internal/document_interaction_state.rb +293 -9
- data/lib/dommy/internal/dom_matching.rb +7 -7
- data/lib/dommy/internal/element_aria.rb +65 -23
- data/lib/dommy/internal/element_css_inline_style.rb +26 -0
- data/lib/dommy/internal/element_editing.rb +41 -0
- data/lib/dommy/internal/element_nonce.rb +28 -0
- data/lib/dommy/internal/element_popover.rb +310 -0
- data/lib/dommy/internal/element_shadow.rb +107 -33
- data/lib/dommy/internal/element_state.rb +131 -26
- data/lib/dommy/internal/element_top_layer.rb +21 -51
- 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 +86 -0
- 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 +36 -0
- data/lib/dommy/internal/input_type.rb +90 -20
- 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/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mime_type.rb +8 -0
- data/lib/dommy/internal/mutation_coordinator.rb +94 -60
- data/lib/dommy/internal/node_adopter.rb +165 -97
- data/lib/dommy/internal/node_factory.rb +111 -49
- data/lib/dommy/internal/node_identity.rb +1 -1
- data/lib/dommy/internal/node_traversal.rb +30 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/observer_options.rb +24 -14
- data/lib/dommy/internal/origin.rb +86 -0
- data/lib/dommy/internal/parent_node.rb +12 -10
- 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 +146 -77
- data/lib/dommy/internal/reflected_attributes.rb +173 -43
- data/lib/dommy/internal/rendered_text/collector.rb +12 -9
- data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
- data/lib/dommy/internal/selector_arguments.rb +2 -16
- data/lib/dommy/internal/selector_index.rb +19 -10
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +91 -256
- data/lib/dommy/internal/selector_parser.rb +176 -100
- data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
- data/lib/dommy/internal/shadow_root_registry.rb +14 -6
- data/lib/dommy/internal/sibling_positions.rb +59 -0
- data/lib/dommy/internal/svg_uri_reference.rb +33 -0
- data/lib/dommy/internal/template_content_registry.rb +11 -5
- 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 +160 -16
- data/lib/dommy/internal/window_constructors.rb +34 -14
- data/lib/dommy/internal/xml_serialization.rb +9 -45
- 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 +51 -15
- data/lib/dommy/js/host_bridge.rb +189 -40
- data/lib/dommy/js/host_callbacks.rb +10 -0
- data/lib/dommy/js/host_runtime.js +1921 -416
- data/lib/dommy/js/marshaller.rb +13 -4
- data/lib/dommy/js/module_loader.rb +46 -5
- data/lib/dommy/js/module_preload.rb +194 -0
- data/lib/dommy/js/runtime.rb +9 -0
- data/lib/dommy/js/script_boot.rb +200 -114
- 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 +72 -101
- data/lib/dommy/js/window_wiring.rb +9 -5
- data/lib/dommy/location.rb +129 -76
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/media_query_list.rb +4 -13
- data/lib/dommy/message_channel.rb +137 -64
- data/lib/dommy/mutation_observer.rb +3 -7
- data/lib/dommy/navigation.rb +61 -11
- data/lib/dommy/navigator.rb +132 -10
- data/lib/dommy/node.rb +81 -44
- data/lib/dommy/notification.rb +10 -6
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +20 -26
- data/lib/dommy/range.rb +20 -47
- data/lib/dommy/resources.rb +38 -3
- data/lib/dommy/scheduler.rb +176 -48
- data/lib/dommy/shadow_root.rb +125 -25
- data/lib/dommy/storage.rb +135 -25
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/structured_data.rb +45 -0
- data/lib/dommy/style_declaration.rb +38 -6
- data/lib/dommy/svg_elements.rb +25 -11
- data/lib/dommy/text_codec.rb +2 -14
- data/lib/dommy/tree_walker.rb +12 -26
- data/lib/dommy/url.rb +12 -9
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +109 -107
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +10 -33
- data/lib/dommy/window.rb +817 -105
- data/lib/dommy/worker.rb +4 -28
- data/lib/dommy/xml_http_request.rb +32 -45
- data/lib/dommy.rb +43 -10
- metadata +52 -5
- data/lib/dommy/backend/makiri_adapter.rb +0 -408
- data/lib/dommy/internal/cookie_jar.rb +0 -29
data/lib/dommy/bridge/values.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Dommy
|
|
4
4
|
module Bridge
|
|
5
5
|
# The Ruby shapes a JS value takes while it is on this side: an opaque
|
|
6
|
-
# reference
|
|
7
|
-
# ArrayBuffer.
|
|
6
|
+
# reference (a Date one carrying its time value), and the two byte buffers
|
|
7
|
+
# that cross as a typed array or a bare ArrayBuffer.
|
|
8
8
|
|
|
9
9
|
# An opaque handle to a JS-side value that Ruby only stores and hands back
|
|
10
10
|
# (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
|
|
@@ -35,6 +35,33 @@ module Dommy
|
|
|
35
35
|
def inspect = "#<Dommy::Bridge::JSValue #{to_s}>"
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# A JS Date that crossed into Ruby. It is still an opaque reference, so
|
|
39
|
+
# handing it back returns the same Date, but it also carries its time value
|
|
40
|
+
# (ms since the epoch, NaN for an invalid date) — the one thing a host API
|
|
41
|
+
# taking a Date (`input.valueAsDate = d`) needs to read off it. A Date the
|
|
42
|
+
# host creates crosses from a Ruby ::Time instead.
|
|
43
|
+
class Date < JSValue
|
|
44
|
+
# The largest time value a JS Date can hold, either side of the epoch.
|
|
45
|
+
MAX_TIME_VALUE = 8_640_000_000_000_000
|
|
46
|
+
|
|
47
|
+
# The time value a JS Date made from `time` holds — ECMAScript's TimeClip:
|
|
48
|
+
# whole milliseconds truncated toward zero (0.1 ms before the epoch is 0,
|
|
49
|
+
# not -1), and NaN beyond MAX_TIME_VALUE.
|
|
50
|
+
def self.time_value_of(time)
|
|
51
|
+
ms = (time.to_r * 1000).truncate
|
|
52
|
+
ms.abs > MAX_TIME_VALUE ? ::Float::NAN : ms.to_f
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :time_value
|
|
56
|
+
|
|
57
|
+
def initialize(ref, time_value)
|
|
58
|
+
super(ref, nil, nil, "Date")
|
|
59
|
+
@time_value = time_value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inspect = "#<Dommy::Bridge::Date #{time_value}>"
|
|
63
|
+
end
|
|
64
|
+
|
|
38
65
|
# A byte buffer that crosses the JS boundary as a `Uint8Array` (rather than a
|
|
39
66
|
# plain Array). Wrap a host method's byte-array result in this so JS sees a
|
|
40
67
|
# real typed array — e.g. `TextEncoder#encode`, `Blob#arrayBuffer`. The
|
|
@@ -25,10 +25,6 @@ module Dommy
|
|
|
25
25
|
# method set) instead of a `__rb_host_describe` round trip per new proxy —
|
|
26
26
|
# the dominant overhead when JS traverses/builds many nodes.
|
|
27
27
|
INTERFACE = "__rb_if"
|
|
28
|
-
# The custom-element tag of a handle whose node is a registered custom
|
|
29
|
-
# element (so makeProxy upgrades it), carried per-instance since it is the
|
|
30
|
-
# one part of a describe that is NOT per-interface.
|
|
31
|
-
CUSTOM_ELEMENT = "__rb_ce"
|
|
32
28
|
# A live JS function that crossed into Ruby, referenced by callback id.
|
|
33
29
|
CALLBACK = "__rb_callback"
|
|
34
30
|
# An opaque JS value referenced by its id in the JS-side `jsRefs` table
|
|
@@ -58,6 +54,10 @@ module Dommy
|
|
|
58
54
|
BYTES = "__rb_bytes"
|
|
59
55
|
# A byte buffer crossing as a bare JS ArrayBuffer.
|
|
60
56
|
ARRAY_BUFFER = "__rb_arraybuffer"
|
|
57
|
+
# A JS Date's time value (ms since the epoch). Host -> JS it builds a new
|
|
58
|
+
# Date; JS -> host it decorates a JS_REF, so the Date keeps its identity
|
|
59
|
+
# and the host can still read the time (see Dommy::Bridge::Date).
|
|
60
|
+
DATE = "__rb_date"
|
|
61
61
|
# A host-raised DOMException/TypeError/RangeError, re-thrown JS-side.
|
|
62
62
|
EXCEPTION = "__rb_exception__"
|
|
63
63
|
# A host-created native JS error (TypeError/RangeError) crossing as a VALUE
|
|
@@ -68,6 +68,10 @@ module Dommy
|
|
|
68
68
|
THROW = "__rb_throw__"
|
|
69
69
|
# A callback whose JS invocation threw (the thrown value is carried here).
|
|
70
70
|
CALLBACK_THREW = "__rb_cb_threw__"
|
|
71
|
+
# A structured-serialization record the JS realm holds (by id), crossing
|
|
72
|
+
# as an opaque Dommy::Js::SerializedRecord (see host_runtime.js
|
|
73
|
+
# serializedTag).
|
|
74
|
+
SERIALIZED = "__rb_serialized"
|
|
71
75
|
end
|
|
72
76
|
end
|
|
73
77
|
end
|
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
|
|
@@ -298,7 +456,7 @@ module Dommy
|
|
|
298
456
|
@runtime&.dispose
|
|
299
457
|
# The JS engine is pluggable: `@backend` selects a registered runtime
|
|
300
458
|
# (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
|
|
301
|
-
runtime = Js.build_runtime(@backend)
|
|
459
|
+
runtime = Js::ModulePreload.build_runtime(window.document, @backend)
|
|
302
460
|
# Every uncaught error the page produces goes through the window's
|
|
303
461
|
# "report an exception" / "notify about rejected promises" funnel, and only
|
|
304
462
|
# what the page left unhandled lands here. A page that installs its own
|
|
@@ -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
|
|
|
@@ -409,7 +661,7 @@ module Dommy
|
|
|
409
661
|
return nil if name.empty? || RESERVED_TARGETS.include?(name.downcase)
|
|
410
662
|
|
|
411
663
|
@window.document.query_selector_all("iframe").find do |frame|
|
|
412
|
-
frame.
|
|
664
|
+
frame.__internal_attribute_value__("name").to_s == name
|
|
413
665
|
end
|
|
414
666
|
end
|
|
415
667
|
|
|
@@ -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=
|
|
@@ -463,9 +726,9 @@ module Dommy
|
|
|
463
726
|
# reloads and is left alone to avoid a busy loop).
|
|
464
727
|
def meta_refresh_target(document)
|
|
465
728
|
document.query_selector_all("meta").each do |meta|
|
|
466
|
-
next unless meta.
|
|
729
|
+
next unless meta.__internal_attribute_value__("http-equiv").to_s.casecmp?("refresh")
|
|
467
730
|
|
|
468
|
-
_delay, separator, rest = meta.
|
|
731
|
+
_delay, separator, rest = meta.__internal_attribute_value__("content").to_s.partition(";")
|
|
469
732
|
next if separator.empty?
|
|
470
733
|
|
|
471
734
|
url = rest.strip.sub(/\Aurl\s*=\s*/i, "").gsub(/\A["']|["']\z/, "").strip
|
|
@@ -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"
|