dommy 0.5.0 → 0.6.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 +30 -4
- data/lib/dommy/animation.rb +288 -0
- data/lib/dommy/compression_streams.rb +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +395 -0
- data/lib/dommy/document.rb +93 -1
- data/lib/dommy/element.rb +131 -9
- data/lib/dommy/event.rb +370 -0
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +62 -0
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/history.rb +79 -0
- data/lib/dommy/html_elements.rb +20 -25
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/dom_matching.rb +1 -1
- 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_wrapper_cache.rb +1 -1
- 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/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +0 -138
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/navigator.rb +361 -1
- data/lib/dommy/notification.rb +89 -0
- 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/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/url.rb +249 -21
- 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/{world.rb → window.rb} +149 -2
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +423 -0
- data/lib/dommy.rb +31 -3
- metadata +34 -5
- /data/lib/dommy/{observer.rb → mutation_observer.rb} +0 -0
data/lib/dommy/crypto.rb
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
module Dommy
|
|
8
|
+
# `Crypto` — mirror of `window.crypto`. Exposes `randomUUID()`,
|
|
9
|
+
# `getRandomValues(typedArray)`, and a minimal `subtle` surface
|
|
10
|
+
# (digest only, sufficient for most test fixtures).
|
|
11
|
+
#
|
|
12
|
+
# Spec: https://w3c.github.io/webcrypto/
|
|
13
|
+
class Crypto
|
|
14
|
+
def initialize(window = nil)
|
|
15
|
+
@window = window
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# JS: crypto.randomUUID() → version-4 UUID string.
|
|
19
|
+
def random_uuid
|
|
20
|
+
SecureRandom.uuid
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias randomUUID random_uuid
|
|
24
|
+
|
|
25
|
+
# JS: crypto.getRandomValues(typedArray) — fills the supplied
|
|
26
|
+
# buffer in place and returns it. JS TypedArrays carry a
|
|
27
|
+
# `byteLength` property; we honor that to fill multi-byte
|
|
28
|
+
# element arrays (Uint16Array, etc.) correctly. Plain Ruby
|
|
29
|
+
# arrays fall back to `size` (1 byte per slot).
|
|
30
|
+
def get_random_values(typed_array)
|
|
31
|
+
return typed_array unless typed_array.respond_to?(:size) && typed_array.respond_to?(:[]=)
|
|
32
|
+
|
|
33
|
+
byte_length = if typed_array.respond_to?(:byteLength)
|
|
34
|
+
typed_array.byteLength
|
|
35
|
+
elsif typed_array.respond_to?(:byte_length)
|
|
36
|
+
typed_array.byte_length
|
|
37
|
+
else
|
|
38
|
+
typed_array.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
bytes_per_element = [byte_length / typed_array.size, 1].max
|
|
42
|
+
bytes = SecureRandom.bytes(byte_length).bytes
|
|
43
|
+
typed_array.size.times do |i|
|
|
44
|
+
offset = i * bytes_per_element
|
|
45
|
+
value = bytes[offset, bytes_per_element].reduce(0) { |acc, b| (acc << 8) | b }
|
|
46
|
+
typed_array[i] = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
typed_array
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias getRandomValues get_random_values
|
|
53
|
+
|
|
54
|
+
def subtle
|
|
55
|
+
@subtle ||= SubtleCrypto.new(@window)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def __js_get__(key)
|
|
59
|
+
case key
|
|
60
|
+
when "subtle"
|
|
61
|
+
subtle
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def __js_call__(method, args)
|
|
66
|
+
case method
|
|
67
|
+
when "randomUUID"
|
|
68
|
+
random_uuid
|
|
69
|
+
when "getRandomValues"
|
|
70
|
+
get_random_values(args[0])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `SubtleCrypto` — `window.crypto.subtle`. Currently covers `digest`
|
|
76
|
+
# (SHA-1 / SHA-256 / SHA-384 / SHA-512), which is by far the most
|
|
77
|
+
# commonly used operation in test contexts. Encrypt / decrypt /
|
|
78
|
+
# sign / verify / key generation are out of scope; tests that
|
|
79
|
+
# need them should mock `crypto.subtle` directly.
|
|
80
|
+
#
|
|
81
|
+
# Returned values are byte arrays — real `SubtleCrypto.digest`
|
|
82
|
+
# resolves to an `ArrayBuffer`; we expose the equivalent Ruby
|
|
83
|
+
# byte array so callers can convert as needed.
|
|
84
|
+
class SubtleCrypto
|
|
85
|
+
ALGORITHMS = {
|
|
86
|
+
"SHA-1" => -> (data) { Digest::SHA1.digest(data) },
|
|
87
|
+
"SHA-256" => -> (data) { Digest::SHA256.digest(data) },
|
|
88
|
+
"SHA-384" => -> (data) { Digest::SHA384.digest(data) },
|
|
89
|
+
"SHA-512" => -> (data) { Digest::SHA512.digest(data) }
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
def initialize(window = nil)
|
|
93
|
+
@window = window
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def digest(algorithm, data)
|
|
97
|
+
promise do
|
|
98
|
+
name = algorithm_name(algorithm)
|
|
99
|
+
hasher = ALGORITHMS[name]
|
|
100
|
+
raise ArgumentError, "unsupported algorithm: #{name}" unless hasher
|
|
101
|
+
|
|
102
|
+
hasher.call(coerce_bytes(data)).bytes
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Generate a fresh symmetric key. `algorithm` is `{name: "HMAC",
|
|
107
|
+
# hash: "SHA-256"}` or `{name: "AES-GCM", length: 128|256}`.
|
|
108
|
+
def generate_key(algorithm, extractable = true, usages = nil)
|
|
109
|
+
promise do
|
|
110
|
+
case primary_algorithm_name(algorithm)
|
|
111
|
+
when "HMAC"
|
|
112
|
+
hash = hmac_hash_from(algorithm)
|
|
113
|
+
CryptoKey.new(
|
|
114
|
+
:secret,
|
|
115
|
+
"HMAC",
|
|
116
|
+
hash,
|
|
117
|
+
SecureRandom.bytes(openssl_digest_size(hash)),
|
|
118
|
+
extractable: extractable,
|
|
119
|
+
usages: usages || %w[sign verify]
|
|
120
|
+
)
|
|
121
|
+
when "AES-GCM", "AES-CBC", "AES-CTR"
|
|
122
|
+
length = (algorithm.is_a?(Hash) && (algorithm["length"] || algorithm[:length])) || 256
|
|
123
|
+
raise ArgumentError, "AES key length must be 128/192/256" unless [128, 192, 256].include?(length)
|
|
124
|
+
|
|
125
|
+
CryptoKey.new(
|
|
126
|
+
:secret,
|
|
127
|
+
primary_algorithm_name(algorithm),
|
|
128
|
+
nil,
|
|
129
|
+
SecureRandom.bytes(length / 8),
|
|
130
|
+
extractable: extractable,
|
|
131
|
+
usages: usages || %w[encrypt decrypt]
|
|
132
|
+
)
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, "unsupported algorithm: #{primary_algorithm_name(algorithm)}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
alias generateKey generate_key
|
|
140
|
+
|
|
141
|
+
# Import a raw key. Supports HMAC and AES-GCM/CBC/CTR.
|
|
142
|
+
def import_key(format, key_data, algorithm, extractable = true, usages = nil)
|
|
143
|
+
promise do
|
|
144
|
+
raise ArgumentError, "only raw format supported" unless format.to_s == "raw"
|
|
145
|
+
|
|
146
|
+
bytes = coerce_bytes(key_data)
|
|
147
|
+
case primary_algorithm_name(algorithm)
|
|
148
|
+
when "HMAC"
|
|
149
|
+
hash = hmac_hash_from(algorithm)
|
|
150
|
+
CryptoKey.new(
|
|
151
|
+
:secret,
|
|
152
|
+
"HMAC",
|
|
153
|
+
hash,
|
|
154
|
+
bytes,
|
|
155
|
+
extractable: extractable,
|
|
156
|
+
usages: usages || %w[sign verify]
|
|
157
|
+
)
|
|
158
|
+
when "AES-GCM", "AES-CBC", "AES-CTR"
|
|
159
|
+
unless [16, 24, 32].include?(bytes.bytesize)
|
|
160
|
+
raise ArgumentError, "AES key must be 16/24/32 bytes"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
CryptoKey.new(
|
|
164
|
+
:secret,
|
|
165
|
+
primary_algorithm_name(algorithm),
|
|
166
|
+
nil,
|
|
167
|
+
bytes,
|
|
168
|
+
extractable: extractable,
|
|
169
|
+
usages: usages || %w[encrypt decrypt]
|
|
170
|
+
)
|
|
171
|
+
else
|
|
172
|
+
raise ArgumentError, "unsupported algorithm: #{primary_algorithm_name(algorithm)}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
alias importKey import_key
|
|
178
|
+
|
|
179
|
+
# HMAC sign — returns the MAC as a byte array.
|
|
180
|
+
def sign(_algorithm, key, data)
|
|
181
|
+
promise do
|
|
182
|
+
raise ArgumentError, "HMAC key required" unless key.is_a?(CryptoKey) && key.algorithm_name == "HMAC"
|
|
183
|
+
raise ArgumentError, "key.usages must include 'sign'" unless key.usages.include?("sign")
|
|
184
|
+
|
|
185
|
+
OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.__bytes__, coerce_bytes(data)).bytes
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# HMAC verify — constant-time compare of the MAC.
|
|
190
|
+
def verify(_algorithm, key, signature, data)
|
|
191
|
+
promise do
|
|
192
|
+
raise ArgumentError, "HMAC key required" unless key.is_a?(CryptoKey) && key.algorithm_name == "HMAC"
|
|
193
|
+
raise ArgumentError, "key.usages must include 'verify'" unless key.usages.include?("verify")
|
|
194
|
+
|
|
195
|
+
expected = OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.__bytes__, coerce_bytes(data))
|
|
196
|
+
sig_bytes = coerce_bytes(signature)
|
|
197
|
+
if expected.bytesize == sig_bytes.bytesize
|
|
198
|
+
OpenSSL.fixed_length_secure_compare(expected, sig_bytes)
|
|
199
|
+
else
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# AES-GCM encrypt. `algorithm` must be `{name: "AES-GCM", iv:
|
|
206
|
+
# <bytes>, additionalData?: <bytes>, tagLength?: 128}`.
|
|
207
|
+
# Output is `ciphertext || authTag`, matching WebCrypto.
|
|
208
|
+
def encrypt(algorithm, key, data)
|
|
209
|
+
promise do
|
|
210
|
+
cipher = build_gcm_cipher(:encrypt, algorithm, key)
|
|
211
|
+
ct = cipher.update(coerce_bytes(data)) + cipher.final
|
|
212
|
+
# OpenSSL always produces a 16-byte tag for GCM; truncate to
|
|
213
|
+
# the requested `tagLength` to honour the spec.
|
|
214
|
+
tag = cipher.auth_tag.byteslice(0, aes_gcm_tag_length(algorithm))
|
|
215
|
+
(ct + tag).bytes
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def decrypt(algorithm, key, data)
|
|
220
|
+
promise do
|
|
221
|
+
bytes = coerce_bytes(data)
|
|
222
|
+
tag_len = aes_gcm_tag_length(algorithm)
|
|
223
|
+
raise ArgumentError, "ciphertext shorter than auth tag" if bytes.bytesize < tag_len
|
|
224
|
+
|
|
225
|
+
ct = bytes.byteslice(0, bytes.bytesize - tag_len)
|
|
226
|
+
tag = bytes.byteslice(bytes.bytesize - tag_len, tag_len)
|
|
227
|
+
cipher = build_gcm_cipher(:decrypt, algorithm, key)
|
|
228
|
+
cipher.auth_tag = tag
|
|
229
|
+
(cipher.update(ct) + cipher.final).bytes
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def __js_call__(method, args)
|
|
234
|
+
case method
|
|
235
|
+
when "digest"
|
|
236
|
+
digest(args[0], args[1])
|
|
237
|
+
when "generateKey"
|
|
238
|
+
generate_key(args[0], args[1], args[2])
|
|
239
|
+
when "importKey"
|
|
240
|
+
import_key(args[0], args[1], args[2], args[3], args[4])
|
|
241
|
+
when "sign"
|
|
242
|
+
sign(args[0], args[1], args[2])
|
|
243
|
+
when "verify"
|
|
244
|
+
verify(args[0], args[1], args[2], args[3])
|
|
245
|
+
when "encrypt"
|
|
246
|
+
encrypt(args[0], args[1], args[2])
|
|
247
|
+
when "decrypt"
|
|
248
|
+
decrypt(args[0], args[1], args[2])
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
# Run `block` synchronously and wrap the result (or raised error)
|
|
255
|
+
# in a `PromiseValue`. Required by the WebCrypto spec — every
|
|
256
|
+
# method is `Promise`-returning even when the underlying work is
|
|
257
|
+
# synchronous.
|
|
258
|
+
def promise(&block)
|
|
259
|
+
result = block.call
|
|
260
|
+
PromiseValue.resolve(@window, result)
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
PromiseValue.reject(@window, e)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def algorithm_name(algorithm)
|
|
266
|
+
raw = algorithm.is_a?(Hash) ? (algorithm["name"] || algorithm[:name]) : algorithm
|
|
267
|
+
s = raw.to_s.upcase
|
|
268
|
+
# Normalize `SHA256` → `SHA-256`; preserve already-hyphenated `SHA-256`.
|
|
269
|
+
return s if s.include?("-")
|
|
270
|
+
return s.sub("SHA", "SHA-") if s.start_with?("SHA")
|
|
271
|
+
|
|
272
|
+
s
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def coerce_bytes(data)
|
|
276
|
+
case data
|
|
277
|
+
when String
|
|
278
|
+
data
|
|
279
|
+
when Array
|
|
280
|
+
data.pack("C*")
|
|
281
|
+
else
|
|
282
|
+
data.respond_to?(:to_a) ? data.to_a.pack("C*") : data.to_s
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Resolve `algorithm["hash"]` to a canonical hash name (`"SHA-256"`
|
|
287
|
+
# etc.). Accepts the spec shapes:
|
|
288
|
+
# "SHA-256" (bare string)
|
|
289
|
+
# {hash: "SHA-256"}
|
|
290
|
+
# {hash: {name: "SHA-256"}}
|
|
291
|
+
# {name: "HMAC", hash: "SHA-256"}
|
|
292
|
+
# Raises `ArgumentError` if no hash can be resolved — unlike some
|
|
293
|
+
# browser UAs, dommy refuses to silently default to SHA-256.
|
|
294
|
+
def hmac_hash_from(algorithm)
|
|
295
|
+
hash_field = hash_descriptor(algorithm)
|
|
296
|
+
name = algorithm_name(hash_field)
|
|
297
|
+
|
|
298
|
+
if name.nil? || name.empty? || name == "HMAC"
|
|
299
|
+
raise ArgumentError, "HMAC requires an explicit hash (e.g. {hash: 'SHA-256'})"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
raise ArgumentError, "unsupported HMAC hash: #{name}" unless ALGORITHMS.key?(name)
|
|
303
|
+
|
|
304
|
+
name
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def hash_descriptor(algorithm)
|
|
308
|
+
return algorithm unless algorithm.is_a?(Hash)
|
|
309
|
+
|
|
310
|
+
algorithm["hash"] || algorithm[:hash] || algorithm
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The algorithm's primary name (`"HMAC"` / `"AES-GCM"` / ...) —
|
|
314
|
+
# ignoring any nested `hash` descriptor.
|
|
315
|
+
def primary_algorithm_name(algorithm)
|
|
316
|
+
raw = algorithm.is_a?(Hash) ? (algorithm["name"] || algorithm[:name]) : algorithm
|
|
317
|
+
algorithm_name(raw)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def openssl_digest_name(hash_name)
|
|
321
|
+
hash_name.sub("SHA-", "SHA")
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def aes_gcm_tag_length(algorithm)
|
|
325
|
+
bits = (algorithm.is_a?(Hash) && (algorithm["tagLength"] || algorithm[:tagLength])) || 128
|
|
326
|
+
bits / 8
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def build_gcm_cipher(direction, algorithm, key)
|
|
330
|
+
raw_key = key.is_a?(CryptoKey) ? key.__bytes__ : coerce_bytes(key)
|
|
331
|
+
raise ArgumentError, "AES-GCM key must be 16/24/32 bytes" unless [16, 24, 32].include?(raw_key.bytesize)
|
|
332
|
+
|
|
333
|
+
iv = algorithm.is_a?(Hash) ? (algorithm["iv"] || algorithm[:iv]) : nil
|
|
334
|
+
raise ArgumentError, "AES-GCM requires an iv" if iv.nil?
|
|
335
|
+
|
|
336
|
+
cipher = OpenSSL::Cipher.new("aes-#{raw_key.bytesize * 8}-gcm")
|
|
337
|
+
direction == :encrypt ? cipher.encrypt : cipher.decrypt
|
|
338
|
+
cipher.key = raw_key
|
|
339
|
+
cipher.iv = coerce_bytes(iv)
|
|
340
|
+
aad = algorithm.is_a?(Hash) ? (algorithm["additionalData"] || algorithm[:additionalData]) : nil
|
|
341
|
+
cipher.auth_data = coerce_bytes(aad) if aad
|
|
342
|
+
cipher
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def openssl_digest_size(hash_name)
|
|
346
|
+
case hash_name
|
|
347
|
+
when "SHA-1"
|
|
348
|
+
20
|
|
349
|
+
when "SHA-256"
|
|
350
|
+
32
|
|
351
|
+
when "SHA-384"
|
|
352
|
+
48
|
|
353
|
+
when "SHA-512"
|
|
354
|
+
64
|
|
355
|
+
else
|
|
356
|
+
32
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# `CryptoKey` — opaque key handle returned by SubtleCrypto.
|
|
362
|
+
# `extractable: false` keys reject export attempts; the
|
|
363
|
+
# `__bytes__` accessor stays internal-only (`__double_underscore__`
|
|
364
|
+
# convention) so production code paths can't read the raw bytes.
|
|
365
|
+
class CryptoKey
|
|
366
|
+
attr_reader :type, :algorithm_name, :hash_name, :usages, :extractable
|
|
367
|
+
|
|
368
|
+
def initialize(type, algorithm_name, hash_name, bytes, extractable: true, usages: [])
|
|
369
|
+
@type = type
|
|
370
|
+
@algorithm_name = algorithm_name
|
|
371
|
+
@hash_name = hash_name
|
|
372
|
+
@bytes = bytes
|
|
373
|
+
@extractable = extractable
|
|
374
|
+
@usages = usages.map(&:to_s).freeze
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Test / internal seam — production callers should not reach in.
|
|
378
|
+
def __bytes__
|
|
379
|
+
@bytes
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def __js_get__(key)
|
|
383
|
+
case key
|
|
384
|
+
when "type"
|
|
385
|
+
@type.to_s
|
|
386
|
+
when "extractable"
|
|
387
|
+
@extractable
|
|
388
|
+
when "algorithm"
|
|
389
|
+
{"name" => @algorithm_name, "hash" => {"name" => @hash_name}}
|
|
390
|
+
when "usages"
|
|
391
|
+
@usages
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
data/lib/dommy/document.rb
CHANGED
|
@@ -256,9 +256,36 @@ module Dommy
|
|
|
256
256
|
alias has_focus has_focus?
|
|
257
257
|
|
|
258
258
|
def get_selection
|
|
259
|
-
|
|
259
|
+
@__selection ||= Selection.new(self)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def create_range
|
|
263
|
+
Range.new(self)
|
|
260
264
|
end
|
|
261
265
|
|
|
266
|
+
# Fullscreen API — no actual fullscreen mode, just track which
|
|
267
|
+
# element claimed it. `element.requestFullscreen()` sets it; this
|
|
268
|
+
# is the read side.
|
|
269
|
+
attr_reader :fullscreen_element
|
|
270
|
+
|
|
271
|
+
def __set_fullscreen_element__(element)
|
|
272
|
+
previous = @fullscreen_element
|
|
273
|
+
@fullscreen_element = element
|
|
274
|
+
return if previous == element
|
|
275
|
+
|
|
276
|
+
dispatch_event(Event.new("fullscreenchange"))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def exit_fullscreen
|
|
280
|
+
return PromiseValue.resolve(@default_view, nil) if @fullscreen_element.nil?
|
|
281
|
+
|
|
282
|
+
@fullscreen_element = nil
|
|
283
|
+
dispatch_event(Event.new("fullscreenchange"))
|
|
284
|
+
PromiseValue.resolve(@default_view, nil)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
alias exitFullscreen exit_fullscreen
|
|
288
|
+
|
|
262
289
|
def element_from_point(_x, _y)
|
|
263
290
|
nil
|
|
264
291
|
end
|
|
@@ -359,6 +386,12 @@ module Dommy
|
|
|
359
386
|
doctype
|
|
360
387
|
when "defaultView"
|
|
361
388
|
@default_view
|
|
389
|
+
when "fullscreenElement"
|
|
390
|
+
@fullscreen_element
|
|
391
|
+
when "fullscreenEnabled"
|
|
392
|
+
true
|
|
393
|
+
when "scrollingElement"
|
|
394
|
+
wrap_node(@nokogiri_doc.at_css("html"))
|
|
362
395
|
when "documentElement"
|
|
363
396
|
wrap_node(@nokogiri_doc.at_css("html"))
|
|
364
397
|
when "title"
|
|
@@ -413,6 +446,20 @@ module Dommy
|
|
|
413
446
|
|
|
414
447
|
def __js_call__(method, args)
|
|
415
448
|
case method
|
|
449
|
+
when "exitFullscreen"
|
|
450
|
+
exit_fullscreen
|
|
451
|
+
when "startViewTransition"
|
|
452
|
+
# View Transitions API stub. Spec: invoke the callback
|
|
453
|
+
# synchronously; return a ViewTransition with already-resolved
|
|
454
|
+
# `finished` / `ready` / `updateCallbackDone` promises.
|
|
455
|
+
callback = args[0]
|
|
456
|
+
if callback.respond_to?(:__js_call__)
|
|
457
|
+
callback.__js_call__("call", [])
|
|
458
|
+
elsif callback.respond_to?(:call)
|
|
459
|
+
callback.call
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
ViewTransition.new(@default_view)
|
|
416
463
|
when "createElement"
|
|
417
464
|
create_element(args[0])
|
|
418
465
|
when "createElementNS"
|
|
@@ -671,4 +718,49 @@ module Dommy
|
|
|
671
718
|
end
|
|
672
719
|
|
|
673
720
|
end
|
|
721
|
+
|
|
722
|
+
# `ViewTransition` — return value of `document.startViewTransition()`.
|
|
723
|
+
# All three Promises (`finished` / `ready` / `updateCallbackDone`)
|
|
724
|
+
# resolve immediately since dommy has no actual paint phase.
|
|
725
|
+
#
|
|
726
|
+
# Spec: https://drafts.csswg.org/css-view-transitions/
|
|
727
|
+
class ViewTransition
|
|
728
|
+
def initialize(window)
|
|
729
|
+
@finished = PromiseValue.resolve(window, nil)
|
|
730
|
+
@ready = PromiseValue.resolve(window, nil)
|
|
731
|
+
@update_callback_done = PromiseValue.resolve(window, nil)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
attr_reader :finished, :ready
|
|
735
|
+
|
|
736
|
+
def update_callback_done
|
|
737
|
+
@update_callback_done
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
alias updateCallbackDone update_callback_done
|
|
741
|
+
|
|
742
|
+
def skip_transition
|
|
743
|
+
nil
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
alias skipTransition skip_transition
|
|
747
|
+
|
|
748
|
+
def __js_get__(key)
|
|
749
|
+
case key
|
|
750
|
+
when "finished"
|
|
751
|
+
@finished
|
|
752
|
+
when "ready"
|
|
753
|
+
@ready
|
|
754
|
+
when "updateCallbackDone"
|
|
755
|
+
@update_callback_done
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def __js_call__(method, _args)
|
|
760
|
+
case method
|
|
761
|
+
when "skipTransition"
|
|
762
|
+
skip_transition
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|
|
674
766
|
end
|