dommy 0.5.0 → 0.7.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 +31 -13
- data/lib/dommy/animation.rb +288 -0
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +396 -0
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +190 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +356 -53
- data/lib/dommy/event.rb +431 -25
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +76 -6
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +82 -0
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +130 -67
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +4 -4
- data/lib/dommy/internal/idna.rb +443 -0
- data/lib/dommy/internal/idna_data.rb +10379 -0
- data/lib/dommy/internal/ipv4_parser.rb +78 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/observable_callback.rb +25 -0
- data/lib/dommy/internal/punycode.rb +202 -0
- data/lib/dommy/internal/range_text_serializer.rb +72 -0
- data/lib/dommy/internal/reflected_attributes.rb +45 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +8 -142
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
- data/lib/dommy/navigator.rb +365 -5
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +89 -0
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +146 -0
- data/lib/dommy/performance_observer.rb +55 -0
- data/lib/dommy/range.rb +597 -0
- data/lib/dommy/resize_observer.rb +53 -0
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +274 -29
- data/lib/dommy/url_pattern.rb +144 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +209 -0
- data/lib/dommy/window.rb +369 -0
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +438 -0
- data/lib/dommy.rb +43 -5
- metadata +44 -29
- data/lib/dommy/world.rb +0 -209
data/lib/dommy/worker.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `Worker` — inline-emulated. Dommy does NOT spin up a separate
|
|
5
|
+
# execution context (no JS engine, no Ruby Thread). Instead:
|
|
6
|
+
#
|
|
7
|
+
# - `new Worker("/path/to/worker.js")` records the URL.
|
|
8
|
+
# - The script body is not executed. Tests install message
|
|
9
|
+
# handlers on the worker-side via `worker.__test_on_message__ { ... }`
|
|
10
|
+
# to simulate behavior.
|
|
11
|
+
# - `worker.postMessage(data)` queues a microtask that delivers
|
|
12
|
+
# to the worker-side handler.
|
|
13
|
+
# - The worker-side handler can call `worker.__test_post_to_main__(data)`
|
|
14
|
+
# to deliver a message back to the main side's `message` event.
|
|
15
|
+
#
|
|
16
|
+
# This is enough surface to test "the app correctly posts/receives
|
|
17
|
+
# via Worker" without a real worker runtime.
|
|
18
|
+
#
|
|
19
|
+
# Spec (real): https://html.spec.whatwg.org/multipage/workers.html
|
|
20
|
+
class Worker
|
|
21
|
+
include EventTarget
|
|
22
|
+
|
|
23
|
+
attr_reader :url
|
|
24
|
+
|
|
25
|
+
def initialize(window, url, _options = nil)
|
|
26
|
+
@window = window
|
|
27
|
+
@url = url.to_s
|
|
28
|
+
@inline_handlers = {}
|
|
29
|
+
@worker_side_handlers = []
|
|
30
|
+
@terminated = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Main-side: post a message to the worker.
|
|
34
|
+
def post_message(data)
|
|
35
|
+
return if @terminated
|
|
36
|
+
|
|
37
|
+
cloned = Dommy.structured_clone(data)
|
|
38
|
+
@window.scheduler.queue_microtask(
|
|
39
|
+
proc do
|
|
40
|
+
@worker_side_handlers.each { |h| invoke(h, [{"data" => cloned}]) }
|
|
41
|
+
end
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias postMessage post_message
|
|
48
|
+
|
|
49
|
+
def terminate
|
|
50
|
+
@terminated = true
|
|
51
|
+
@worker_side_handlers.clear
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# --- Test seams (worker-side) ----------------------------------
|
|
56
|
+
|
|
57
|
+
# Register a callback that runs in the "worker side". Multiple
|
|
58
|
+
# registrations stack.
|
|
59
|
+
def __test_on_message__(&block)
|
|
60
|
+
@worker_side_handlers << block
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Worker-side: deliver a message to the main-side `message` event.
|
|
64
|
+
def __test_post_to_main__(data)
|
|
65
|
+
cloned = Dommy.structured_clone(data)
|
|
66
|
+
@window.scheduler.queue_microtask(
|
|
67
|
+
proc do
|
|
68
|
+
dispatch_event(MessageEvent.new("message", "data" => cloned))
|
|
69
|
+
end
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# --- JS bridge -------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def __js_get__(key)
|
|
78
|
+
case key
|
|
79
|
+
when "url"
|
|
80
|
+
@url
|
|
81
|
+
else
|
|
82
|
+
@inline_handlers[inline_event_for(key)]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def __js_set__(key, value)
|
|
87
|
+
event = inline_event_for(key)
|
|
88
|
+
set_inline_handler(event, value) if event
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def __js_call__(method, args)
|
|
93
|
+
case method
|
|
94
|
+
when "postMessage"
|
|
95
|
+
post_message(args[0])
|
|
96
|
+
when "terminate"
|
|
97
|
+
terminate
|
|
98
|
+
when "addEventListener"
|
|
99
|
+
add_event_listener(args[0], args[1], args[2])
|
|
100
|
+
when "removeEventListener"
|
|
101
|
+
remove_event_listener(args[0], args[1])
|
|
102
|
+
when "dispatchEvent"
|
|
103
|
+
dispatch_event(args[0])
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def __internal_event_parent__
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
INLINE_HANDLERS = %w[message error messageerror].freeze
|
|
114
|
+
INLINE_EVENT_MAP = INLINE_HANDLERS
|
|
115
|
+
.each_with_object({}) do |name, h|
|
|
116
|
+
h["on#{name}"] = name
|
|
117
|
+
end
|
|
118
|
+
.freeze
|
|
119
|
+
|
|
120
|
+
def inline_event_for(key)
|
|
121
|
+
INLINE_EVENT_MAP[key.to_s]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def set_inline_handler(event, handler)
|
|
125
|
+
previous = @inline_handlers[event]
|
|
126
|
+
remove_event_listener(event, previous) if previous
|
|
127
|
+
if handler.nil?
|
|
128
|
+
@inline_handlers.delete(event)
|
|
129
|
+
else
|
|
130
|
+
add_event_listener(event, handler)
|
|
131
|
+
@inline_handlers[event] = handler
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def invoke(callback, args)
|
|
136
|
+
if callback.respond_to?(:__js_call__)
|
|
137
|
+
callback.__js_call__("call", args)
|
|
138
|
+
elsif callback.respond_to?(:call)
|
|
139
|
+
callback.call(*args)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
# `XMLHttpRequest` polyfill. Consults the same stub maps that
|
|
7
|
+
# `FetchFn` reads (`__fetchy_stub__` / `__resource_fetch_stub__` /
|
|
8
|
+
# `__inject_fetch_stub__`) so a single set of fixtures drives both
|
|
9
|
+
# `fetch(...)` and `new XMLHttpRequest()` style code.
|
|
10
|
+
#
|
|
11
|
+
# State transitions match the spec:
|
|
12
|
+
# UNSENT(0) → OPENED(1) → HEADERS_RECEIVED(2) → LOADING(3) → DONE(4)
|
|
13
|
+
# Each transition fires `readystatechange`. `load` / `loadend` fire
|
|
14
|
+
# on completion; `error` / `timeout` / `abort` fire on the
|
|
15
|
+
# respective failure paths.
|
|
16
|
+
#
|
|
17
|
+
# Async requests resolve via the scheduler (a microtask, or a
|
|
18
|
+
# `setTimeout` for stubs with `delay:`); sync requests
|
|
19
|
+
# (`open(..., false)`) deliver inline so tests can read
|
|
20
|
+
# `xhr.responseText` immediately.
|
|
21
|
+
#
|
|
22
|
+
# Spec: https://xhr.spec.whatwg.org/
|
|
23
|
+
class XMLHttpRequest
|
|
24
|
+
include EventTarget
|
|
25
|
+
|
|
26
|
+
UNSENT = 0
|
|
27
|
+
OPENED = 1
|
|
28
|
+
HEADERS_RECEIVED = 2
|
|
29
|
+
LOADING = 3
|
|
30
|
+
DONE = 4
|
|
31
|
+
|
|
32
|
+
INLINE_HANDLERS = %w[
|
|
33
|
+
readystatechange
|
|
34
|
+
loadstart
|
|
35
|
+
load
|
|
36
|
+
loadend
|
|
37
|
+
progress
|
|
38
|
+
error
|
|
39
|
+
timeout
|
|
40
|
+
abort
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
attr_reader(
|
|
44
|
+
:ready_state,
|
|
45
|
+
:status,
|
|
46
|
+
:status_text,
|
|
47
|
+
:response_url,
|
|
48
|
+
:response_text,
|
|
49
|
+
:response_xml,
|
|
50
|
+
:response,
|
|
51
|
+
:upload
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
attr_accessor :timeout, :with_credentials, :response_type
|
|
55
|
+
|
|
56
|
+
def initialize(window)
|
|
57
|
+
@window = window
|
|
58
|
+
@timeout = 0
|
|
59
|
+
@with_credentials = false
|
|
60
|
+
@response_type = ""
|
|
61
|
+
@generation = 0
|
|
62
|
+
reset_state
|
|
63
|
+
@inline_handlers = {}
|
|
64
|
+
@upload = XMLHttpRequestUpload.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# XHR §open. `method` is uppercased; `async` defaults to true.
|
|
68
|
+
def open(method, url, async = true, _user = nil, _password = nil)
|
|
69
|
+
reset_state
|
|
70
|
+
@method = method.to_s.upcase
|
|
71
|
+
@url = url.to_s
|
|
72
|
+
@async = async.nil? ? true : !!async
|
|
73
|
+
@request_headers = {}
|
|
74
|
+
transition(OPENED)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_request_header(name, value)
|
|
79
|
+
raise Error, "setRequestHeader called before open" if @ready_state != OPENED
|
|
80
|
+
|
|
81
|
+
key = name.to_s
|
|
82
|
+
existing = @request_headers[key]
|
|
83
|
+
@request_headers[key] = existing ? "#{existing}, #{value}" : value.to_s
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
alias setRequestHeader set_request_header
|
|
88
|
+
|
|
89
|
+
def send(body = nil)
|
|
90
|
+
raise Error, "send called before open" if @ready_state != OPENED
|
|
91
|
+
|
|
92
|
+
@request_body = body
|
|
93
|
+
@sent = true
|
|
94
|
+
@generation += 1
|
|
95
|
+
gen = @generation
|
|
96
|
+
dispatch_event(ProgressEvent.new("loadstart"))
|
|
97
|
+
|
|
98
|
+
entry = lookup_stub
|
|
99
|
+
track_globals
|
|
100
|
+
|
|
101
|
+
if entry.nil?
|
|
102
|
+
deliver(body: "not found", status: 404, status_text: "Not Found", headers: {})
|
|
103
|
+
return nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
delay = entry["delay"]
|
|
107
|
+
if delay && @async
|
|
108
|
+
schedule_delivery_with_delay(entry, delay.to_i, gen)
|
|
109
|
+
elsif @async
|
|
110
|
+
@window.scheduler.queue_microtask(proc { deliver_entry(entry) if active?(gen) })
|
|
111
|
+
else
|
|
112
|
+
deliver_entry(entry)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def abort
|
|
119
|
+
return if @ready_state == UNSENT || @ready_state == DONE
|
|
120
|
+
# WHATWG: abort() is a no-op when in OPENED with the send()
|
|
121
|
+
# flag unset. Without this guard, `xhr.open(); xhr.abort()`
|
|
122
|
+
# would fire abort + loadend even though no request is
|
|
123
|
+
# in flight.
|
|
124
|
+
return if @ready_state == OPENED && !@sent
|
|
125
|
+
|
|
126
|
+
@aborted = true
|
|
127
|
+
@generation += 1
|
|
128
|
+
@status = 0
|
|
129
|
+
@status_text = ""
|
|
130
|
+
transition(DONE)
|
|
131
|
+
dispatch_event(ProgressEvent.new("abort"))
|
|
132
|
+
dispatch_event(ProgressEvent.new("loadend"))
|
|
133
|
+
reset_state(keep_handlers: true, keep_generation: true)
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def get_response_header(name)
|
|
138
|
+
return nil if @ready_state < HEADERS_RECEIVED
|
|
139
|
+
|
|
140
|
+
key = name.to_s.downcase
|
|
141
|
+
hit = @response_headers.find { |k, _| k.to_s.downcase == key }
|
|
142
|
+
hit ? hit.last : nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
alias getResponseHeader get_response_header
|
|
146
|
+
|
|
147
|
+
def get_all_response_headers
|
|
148
|
+
return "" if @ready_state < HEADERS_RECEIVED
|
|
149
|
+
|
|
150
|
+
@response_headers.map { |k, v| "#{k}: #{v}\r\n" }.join
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
alias getAllResponseHeaders get_all_response_headers
|
|
154
|
+
|
|
155
|
+
def override_mime_type(mime)
|
|
156
|
+
@override_mime = mime.to_s
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
alias overrideMimeType override_mime_type
|
|
161
|
+
|
|
162
|
+
# --- JS bridge ---------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def __js_get__(key)
|
|
165
|
+
case key
|
|
166
|
+
when "readyState"
|
|
167
|
+
@ready_state
|
|
168
|
+
when "status"
|
|
169
|
+
@status
|
|
170
|
+
when "statusText"
|
|
171
|
+
@status_text
|
|
172
|
+
when "responseURL"
|
|
173
|
+
@response_url
|
|
174
|
+
when "response"
|
|
175
|
+
@response
|
|
176
|
+
when "responseText"
|
|
177
|
+
@response_text
|
|
178
|
+
when "responseXML"
|
|
179
|
+
@response_xml
|
|
180
|
+
when "responseType"
|
|
181
|
+
@response_type
|
|
182
|
+
when "timeout"
|
|
183
|
+
@timeout
|
|
184
|
+
when "withCredentials"
|
|
185
|
+
@with_credentials
|
|
186
|
+
when "upload"
|
|
187
|
+
@upload
|
|
188
|
+
when "UNSENT"
|
|
189
|
+
UNSENT
|
|
190
|
+
when "OPENED"
|
|
191
|
+
OPENED
|
|
192
|
+
when "HEADERS_RECEIVED"
|
|
193
|
+
HEADERS_RECEIVED
|
|
194
|
+
when "LOADING"
|
|
195
|
+
LOADING
|
|
196
|
+
when "DONE"
|
|
197
|
+
DONE
|
|
198
|
+
else
|
|
199
|
+
@inline_handlers[inline_event_for(key)]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def __js_set__(key, value)
|
|
204
|
+
case key
|
|
205
|
+
when "responseType"
|
|
206
|
+
@response_type = value.to_s
|
|
207
|
+
when "timeout"
|
|
208
|
+
@timeout = value.to_i
|
|
209
|
+
when "withCredentials"
|
|
210
|
+
@with_credentials = !!value
|
|
211
|
+
else
|
|
212
|
+
event = inline_event_for(key)
|
|
213
|
+
set_inline_handler(event, value) if event
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def __js_call__(method, args)
|
|
220
|
+
case method
|
|
221
|
+
when "open"
|
|
222
|
+
open(args[0], args[1], args[2].nil? ? true : args[2], args[3], args[4])
|
|
223
|
+
when "send"
|
|
224
|
+
send(args[0])
|
|
225
|
+
when "setRequestHeader"
|
|
226
|
+
set_request_header(args[0], args[1])
|
|
227
|
+
when "abort"
|
|
228
|
+
abort
|
|
229
|
+
when "getResponseHeader"
|
|
230
|
+
get_response_header(args[0])
|
|
231
|
+
when "getAllResponseHeaders"
|
|
232
|
+
get_all_response_headers
|
|
233
|
+
when "overrideMimeType"
|
|
234
|
+
override_mime_type(args[0])
|
|
235
|
+
when "addEventListener"
|
|
236
|
+
add_event_listener(args[0], args[1], args[2])
|
|
237
|
+
when "removeEventListener"
|
|
238
|
+
remove_event_listener(args[0], args[1])
|
|
239
|
+
when "dispatchEvent"
|
|
240
|
+
dispatch_event(args[0])
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def __internal_event_parent__
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
class Error < StandardError
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def reset_state(keep_handlers: false, keep_generation: false)
|
|
254
|
+
@ready_state = UNSENT
|
|
255
|
+
@status = 0
|
|
256
|
+
@status_text = ""
|
|
257
|
+
@response_url = ""
|
|
258
|
+
@response = nil
|
|
259
|
+
@response_text = ""
|
|
260
|
+
@response_xml = nil
|
|
261
|
+
@response_headers = {}
|
|
262
|
+
@request_headers = {}
|
|
263
|
+
@aborted = false unless keep_generation
|
|
264
|
+
@sent = false
|
|
265
|
+
@override_mime = nil
|
|
266
|
+
@method = nil
|
|
267
|
+
@url = nil
|
|
268
|
+
@async = true
|
|
269
|
+
@inline_handlers = {} unless keep_handlers
|
|
270
|
+
@generation = 0 unless keep_generation
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A queued delivery is "active" only if no abort / reopen has
|
|
274
|
+
# bumped the generation since its send() call.
|
|
275
|
+
def active?(gen)
|
|
276
|
+
@generation == gen && !@aborted
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def transition(state)
|
|
280
|
+
@ready_state = state
|
|
281
|
+
dispatch_event(Event.new("readystatechange"))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def lookup_stub
|
|
285
|
+
stub_map = @window.globals["__fetchy_stub__"] ||
|
|
286
|
+
@window.globals["__resource_fetch_stub__"] ||
|
|
287
|
+
@window.globals["__inject_fetch_stub__"]
|
|
288
|
+
return nil unless stub_map.is_a?(Hash)
|
|
289
|
+
|
|
290
|
+
stub_map[@url]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Bookkeeping that lets specs assert "fetch was called N times"
|
|
294
|
+
# uniformly across `fetch` and XHR (`FetchFn` writes the same
|
|
295
|
+
# globals; XHR mirrors them).
|
|
296
|
+
def track_globals
|
|
297
|
+
@window.globals["__fetch_count__"] = (@window.globals["__fetch_count__"] || 0).to_i + 1
|
|
298
|
+
@window.globals["__last_url__"] = @url
|
|
299
|
+
@window.globals["__last_method__"] = @method
|
|
300
|
+
@window.globals["__last_body__"] = @request_body
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def schedule_delivery_with_delay(entry, delay_ms, gen)
|
|
304
|
+
timer = @window.scheduler.set_timeout(
|
|
305
|
+
proc { deliver_entry(entry) if active?(gen) },
|
|
306
|
+
delay_ms
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return unless @timeout.to_i.positive?
|
|
310
|
+
|
|
311
|
+
@window.scheduler.set_timeout(
|
|
312
|
+
proc {
|
|
313
|
+
next unless active?(gen)
|
|
314
|
+
next if @ready_state == DONE
|
|
315
|
+
|
|
316
|
+
@window.scheduler.clear_timeout(timer)
|
|
317
|
+
fail_with("timeout")
|
|
318
|
+
},
|
|
319
|
+
@timeout
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def deliver_entry(entry)
|
|
324
|
+
body = entry["body"].to_s
|
|
325
|
+
status = (entry["status"] || 200).to_i
|
|
326
|
+
status_text = entry["statusText"] || ""
|
|
327
|
+
headers = entry["headers"] || {"Content-Type" => entry["contentType"] || "text/plain"}
|
|
328
|
+
deliver(body: body, status: status, status_text: status_text, headers: headers)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def deliver(body:, status:, status_text:, headers:)
|
|
332
|
+
return if @aborted
|
|
333
|
+
|
|
334
|
+
@status = status
|
|
335
|
+
@status_text = status_text
|
|
336
|
+
@response_headers = headers
|
|
337
|
+
@response_url = @url
|
|
338
|
+
@response_text = body
|
|
339
|
+
@response = decode_response(body)
|
|
340
|
+
|
|
341
|
+
transition(HEADERS_RECEIVED)
|
|
342
|
+
transition(LOADING)
|
|
343
|
+
transition(DONE)
|
|
344
|
+
dispatch_event(ProgressEvent.new("load"))
|
|
345
|
+
dispatch_event(ProgressEvent.new("loadend"))
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def fail_with(reason)
|
|
349
|
+
@status = 0
|
|
350
|
+
@status_text = ""
|
|
351
|
+
transition(DONE)
|
|
352
|
+
dispatch_event(ProgressEvent.new(reason))
|
|
353
|
+
dispatch_event(ProgressEvent.new("loadend"))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Decode the body into `response` per `responseType`.
|
|
357
|
+
def decode_response(body)
|
|
358
|
+
case @response_type
|
|
359
|
+
when "", "text"
|
|
360
|
+
body
|
|
361
|
+
when "json"
|
|
362
|
+
begin
|
|
363
|
+
JSON.parse(body)
|
|
364
|
+
rescue JSON::ParserError
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
when "arraybuffer"
|
|
369
|
+
body.bytes
|
|
370
|
+
when "blob"
|
|
371
|
+
Blob.new([body], "type" => response_content_type)
|
|
372
|
+
when "document"
|
|
373
|
+
parse_document(body)
|
|
374
|
+
else
|
|
375
|
+
body
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Content-Type of the received response, read straight from
|
|
380
|
+
# `@response_headers` (not `get_response_header`, which gates on
|
|
381
|
+
# `readyState` — decode runs before that flag advances).
|
|
382
|
+
def response_content_type
|
|
383
|
+
hit = @response_headers.find { |k, _| k.to_s.downcase == "content-type" }
|
|
384
|
+
hit ? hit.last.to_s : ""
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def parse_document(body)
|
|
388
|
+
DOMParser.new.parse_from_string(body, "text/html")
|
|
389
|
+
rescue StandardError
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
INLINE_EVENT_MAP = INLINE_HANDLERS
|
|
394
|
+
.each_with_object({}) do |name, h|
|
|
395
|
+
h["on#{name}"] = name
|
|
396
|
+
end
|
|
397
|
+
.freeze
|
|
398
|
+
|
|
399
|
+
def inline_event_for(key)
|
|
400
|
+
INLINE_EVENT_MAP[key.to_s]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def set_inline_handler(event, handler)
|
|
404
|
+
previous = @inline_handlers[event]
|
|
405
|
+
remove_event_listener(event, previous) if previous
|
|
406
|
+
|
|
407
|
+
if handler.nil?
|
|
408
|
+
@inline_handlers.delete(event)
|
|
409
|
+
else
|
|
410
|
+
add_event_listener(event, handler)
|
|
411
|
+
@inline_handlers[event] = handler
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# `XMLHttpRequestUpload` — the upload-side event target. Real
|
|
417
|
+
# browsers fire `progress` here while uploading multipart bodies;
|
|
418
|
+
# dommy doesn't simulate upload, so this is an inert EventTarget
|
|
419
|
+
# the caller can still `addEventListener` against.
|
|
420
|
+
class XMLHttpRequestUpload
|
|
421
|
+
include EventTarget
|
|
422
|
+
|
|
423
|
+
def __js_call__(method, args)
|
|
424
|
+
case method
|
|
425
|
+
when "addEventListener"
|
|
426
|
+
add_event_listener(args[0], args[1], args[2])
|
|
427
|
+
when "removeEventListener"
|
|
428
|
+
remove_event_listener(args[0], args[1])
|
|
429
|
+
when "dispatchEvent"
|
|
430
|
+
dispatch_event(args[0])
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def __internal_event_parent__
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
data/lib/dommy.rb
CHANGED
|
@@ -1,29 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
3
|
require "set"
|
|
5
4
|
|
|
6
5
|
require_relative "dommy/version"
|
|
6
|
+
require_relative "dommy/backend"
|
|
7
7
|
require_relative "dommy/dom_exception"
|
|
8
8
|
require_relative "dommy/node"
|
|
9
9
|
require_relative "dommy/html_collection"
|
|
10
10
|
require_relative "dommy/event"
|
|
11
11
|
require_relative "dommy/scheduler"
|
|
12
|
-
require_relative "dommy/
|
|
12
|
+
require_relative "dommy/mutation_observer"
|
|
13
13
|
require_relative "dommy/promise"
|
|
14
14
|
require_relative "dommy/blob"
|
|
15
15
|
require_relative "dommy/data_transfer"
|
|
16
|
+
require_relative "dommy/crypto"
|
|
17
|
+
require_relative "dommy/text_codec"
|
|
18
|
+
require_relative "dommy/internal/observable_callback"
|
|
19
|
+
require_relative "dommy/internal/css_pseudo_handlers"
|
|
20
|
+
require_relative "dommy/internal/punycode"
|
|
21
|
+
require_relative "dommy/internal/idna"
|
|
22
|
+
require_relative "dommy/internal/ipv4_parser"
|
|
23
|
+
require_relative "dommy/intersection_observer"
|
|
24
|
+
require_relative "dommy/resize_observer"
|
|
25
|
+
require_relative "dommy/performance_observer"
|
|
26
|
+
require_relative "dommy/internal/range_text_serializer"
|
|
27
|
+
require_relative "dommy/range"
|
|
28
|
+
require_relative "dommy/animation"
|
|
16
29
|
require_relative "dommy/bridge"
|
|
17
30
|
require_relative "dommy/storage"
|
|
18
31
|
require_relative "dommy/fetch"
|
|
19
|
-
require_relative "dommy/
|
|
32
|
+
require_relative "dommy/xml_http_request"
|
|
33
|
+
require_relative "dommy/file_reader"
|
|
34
|
+
require_relative "dommy/media_query_list"
|
|
35
|
+
require_relative "dommy/notification"
|
|
36
|
+
require_relative "dommy/message_channel"
|
|
37
|
+
require_relative "dommy/web_socket"
|
|
38
|
+
require_relative "dommy/event_source"
|
|
39
|
+
require_relative "dommy/performance"
|
|
40
|
+
require_relative "dommy/cookie_store"
|
|
41
|
+
require_relative "dommy/url_pattern"
|
|
42
|
+
require_relative "dommy/streams"
|
|
43
|
+
require_relative "dommy/compression_streams"
|
|
44
|
+
require_relative "dommy/worker"
|
|
45
|
+
require_relative "dommy/location"
|
|
46
|
+
require_relative "dommy/history"
|
|
20
47
|
require_relative "dommy/navigator"
|
|
21
48
|
require_relative "dommy/parser"
|
|
22
49
|
require_relative "dommy/attr"
|
|
23
|
-
require_relative "dommy/
|
|
50
|
+
require_relative "dommy/window"
|
|
24
51
|
require_relative "dommy/document"
|
|
25
52
|
require_relative "dommy/element"
|
|
53
|
+
require_relative "dommy/internal/reflected_attributes"
|
|
26
54
|
require_relative "dommy/html_elements"
|
|
55
|
+
require_relative "dommy/svg_elements"
|
|
27
56
|
require_relative "dommy/shadow_root"
|
|
28
57
|
require_relative "dommy/custom_elements"
|
|
29
58
|
require_relative "dommy/tree_walker"
|
|
@@ -45,7 +74,7 @@ module Dommy
|
|
|
45
74
|
def self.parse(html)
|
|
46
75
|
s = html.to_s
|
|
47
76
|
if s.match?(/\A\s*(<!doctype|<html\b)/i)
|
|
48
|
-
Window.new(nil, nokogiri_doc:
|
|
77
|
+
Window.new(nil, nokogiri_doc: Backend.parse(s))
|
|
49
78
|
else
|
|
50
79
|
window = Window.new
|
|
51
80
|
window.document.body.inner_html = s
|
|
@@ -53,6 +82,15 @@ module Dommy
|
|
|
53
82
|
end
|
|
54
83
|
end
|
|
55
84
|
|
|
85
|
+
# Convenience accessor: `Dommy.backend` / `Dommy.backend=`.
|
|
86
|
+
def self.backend
|
|
87
|
+
Backend.current
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.backend=(new_backend)
|
|
91
|
+
Backend.current = new_backend
|
|
92
|
+
end
|
|
93
|
+
|
|
56
94
|
# Build a fresh, empty Window (no host). Equivalent to opening a
|
|
57
95
|
# blank document.
|
|
58
96
|
def self.new_window
|