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/navigator.rb
CHANGED
|
@@ -21,9 +21,51 @@ module Dommy
|
|
|
21
21
|
@cookie_enabled = true
|
|
22
22
|
@clipboard = Clipboard.new(window)
|
|
23
23
|
@permissions = Permissions.new(window)
|
|
24
|
+
@geolocation = Geolocation.new(window)
|
|
25
|
+
@vibration_log = []
|
|
26
|
+
@wake_lock = WakeLock.new(window)
|
|
27
|
+
@locks = LockManager.new(window)
|
|
28
|
+
@storage = StorageManager.new(window)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
|
-
attr_reader :clipboard, :permissions
|
|
31
|
+
attr_reader :clipboard, :permissions, :geolocation, :wake_lock, :locks, :storage
|
|
32
|
+
|
|
33
|
+
# Web Share API. Returns a Promise; tests can inspect
|
|
34
|
+
# `__test_last_shared__` to verify what was offered.
|
|
35
|
+
def share(data = nil)
|
|
36
|
+
@last_shared = data
|
|
37
|
+
PromiseValue.resolve(@window, nil)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def can_share(_data = nil)
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias canShare can_share
|
|
45
|
+
|
|
46
|
+
# Vibration API. No-op in dommy, but the requested pattern is
|
|
47
|
+
# recorded so tests can assert "we asked to vibrate".
|
|
48
|
+
def vibrate(pattern)
|
|
49
|
+
list = pattern.is_a?(Array) ? pattern : [pattern]
|
|
50
|
+
@vibration_log << list.map(&:to_i)
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def __test_vibration_log__
|
|
55
|
+
@vibration_log.dup
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def __test_last_shared__
|
|
59
|
+
@last_shared
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Battery Status API stub. Returns a Promise resolving to a fixed
|
|
63
|
+
# `BatteryManager` snapshot.
|
|
64
|
+
def get_battery
|
|
65
|
+
PromiseValue.resolve(@window, BatteryManager.new)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias getBattery get_battery
|
|
27
69
|
|
|
28
70
|
def [](key)
|
|
29
71
|
__js_get__(key.to_s)
|
|
@@ -53,6 +95,27 @@ module Dommy
|
|
|
53
95
|
@clipboard
|
|
54
96
|
when "permissions"
|
|
55
97
|
@permissions
|
|
98
|
+
when "geolocation"
|
|
99
|
+
@geolocation
|
|
100
|
+
when "wakeLock"
|
|
101
|
+
@wake_lock
|
|
102
|
+
when "locks"
|
|
103
|
+
@locks
|
|
104
|
+
when "storage"
|
|
105
|
+
@storage
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def __js_call__(method, args)
|
|
110
|
+
case method
|
|
111
|
+
when "share"
|
|
112
|
+
share(args[0])
|
|
113
|
+
when "canShare"
|
|
114
|
+
can_share(args[0])
|
|
115
|
+
when "vibrate"
|
|
116
|
+
vibrate(args[0])
|
|
117
|
+
when "getBattery"
|
|
118
|
+
get_battery
|
|
56
119
|
end
|
|
57
120
|
end
|
|
58
121
|
|
|
@@ -130,7 +193,7 @@ module Dommy
|
|
|
130
193
|
end
|
|
131
194
|
end
|
|
132
195
|
|
|
133
|
-
def
|
|
196
|
+
def __internal_event_parent__
|
|
134
197
|
nil
|
|
135
198
|
end
|
|
136
199
|
end
|
|
@@ -174,7 +237,7 @@ module Dommy
|
|
|
174
237
|
@overrides[key] = state.to_s
|
|
175
238
|
@statuses ||= {}
|
|
176
239
|
status = @statuses[key]
|
|
177
|
-
status&.
|
|
240
|
+
status&.__internal_set_state__(state.to_s)
|
|
178
241
|
nil
|
|
179
242
|
end
|
|
180
243
|
|
|
@@ -223,7 +286,7 @@ module Dommy
|
|
|
223
286
|
@onchange = nil
|
|
224
287
|
end
|
|
225
288
|
|
|
226
|
-
def
|
|
289
|
+
def __internal_set_state__(new_state)
|
|
227
290
|
return if @state == new_state
|
|
228
291
|
|
|
229
292
|
@state = new_state
|
|
@@ -264,8 +327,305 @@ module Dommy
|
|
|
264
327
|
end
|
|
265
328
|
end
|
|
266
329
|
|
|
267
|
-
def
|
|
330
|
+
def __internal_event_parent__
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# `navigator.geolocation` — stub Geolocation API. Real implementations
|
|
336
|
+
# query the OS; dommy holds a mock position tests configure via
|
|
337
|
+
# `__test_set_position__(coords)` or `__test_set_error__(error_code)`.
|
|
338
|
+
#
|
|
339
|
+
# Spec: https://www.w3.org/TR/geolocation/
|
|
340
|
+
class Geolocation
|
|
341
|
+
DEFAULT_COORDS = {
|
|
342
|
+
"latitude" => 0.0,
|
|
343
|
+
"longitude" => 0.0,
|
|
344
|
+
"accuracy" => 0.0,
|
|
345
|
+
"altitude" => nil,
|
|
346
|
+
"altitudeAccuracy" => nil,
|
|
347
|
+
"heading" => nil,
|
|
348
|
+
"speed" => nil
|
|
349
|
+
}.freeze
|
|
350
|
+
|
|
351
|
+
def initialize(window)
|
|
352
|
+
@window = window
|
|
353
|
+
@position = nil
|
|
354
|
+
@error = nil
|
|
355
|
+
@watches = {}
|
|
356
|
+
@next_watch_id = 0
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Test seam: install a mock position.
|
|
360
|
+
def __test_set_position__(coords = {})
|
|
361
|
+
merged = DEFAULT_COORDS.merge(coords.transform_keys(&:to_s))
|
|
362
|
+
@position = {"coords" => merged, "timestamp" => @window.scheduler.now_ms}
|
|
363
|
+
@error = nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Test seam: install a permission/positioning error (code 1=PERMISSION_DENIED,
|
|
367
|
+
# 2=POSITION_UNAVAILABLE, 3=TIMEOUT).
|
|
368
|
+
def __test_set_error__(code, message = "")
|
|
369
|
+
@position = nil
|
|
370
|
+
@error = {"code" => code.to_i, "message" => message.to_s}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def get_current_position(success, failure = nil, _options = nil)
|
|
374
|
+
@window.scheduler.queue_microtask(proc { deliver(success, failure) })
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
alias getCurrentPosition get_current_position
|
|
379
|
+
|
|
380
|
+
def watch_position(success, failure = nil, _options = nil)
|
|
381
|
+
id = (@next_watch_id += 1)
|
|
382
|
+
@watches[id] = [success, failure]
|
|
383
|
+
@window.scheduler.queue_microtask(proc { deliver(success, failure) })
|
|
384
|
+
id
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
alias watchPosition watch_position
|
|
388
|
+
|
|
389
|
+
def clear_watch(id)
|
|
390
|
+
@watches.delete(id)
|
|
391
|
+
nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
alias clearWatch clear_watch
|
|
395
|
+
|
|
396
|
+
def __js_call__(method, args)
|
|
397
|
+
case method
|
|
398
|
+
when "getCurrentPosition"
|
|
399
|
+
get_current_position(args[0], args[1], args[2])
|
|
400
|
+
when "watchPosition"
|
|
401
|
+
watch_position(args[0], args[1], args[2])
|
|
402
|
+
when "clearWatch"
|
|
403
|
+
clear_watch(args[0])
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
private
|
|
408
|
+
|
|
409
|
+
def deliver(success, failure)
|
|
410
|
+
if @position
|
|
411
|
+
invoke(success, @position)
|
|
412
|
+
else
|
|
413
|
+
invoke(failure, @error || {"code" => 2, "message" => "POSITION_UNAVAILABLE"})
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def invoke(callback, payload)
|
|
418
|
+
return if callback.nil?
|
|
419
|
+
|
|
420
|
+
if callback.respond_to?(:__js_call__)
|
|
421
|
+
callback.__js_call__("call", [payload])
|
|
422
|
+
elsif callback.respond_to?(:call)
|
|
423
|
+
callback.call(payload)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# `navigator.wakeLock` — Screen Wake Lock API stub. `request(type)`
|
|
429
|
+
# returns a Promise of a `WakeLockSentinel` whose `release()` flips
|
|
430
|
+
# `released` and dispatches a `release` event.
|
|
431
|
+
#
|
|
432
|
+
# Spec: https://www.w3.org/TR/screen-wake-lock/
|
|
433
|
+
class WakeLock
|
|
434
|
+
def initialize(window)
|
|
435
|
+
@window = window
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def request(type = "screen")
|
|
439
|
+
PromiseValue.resolve(@window, WakeLockSentinel.new(@window, type.to_s))
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def __js_call__(method, args)
|
|
443
|
+
case method
|
|
444
|
+
when "request"
|
|
445
|
+
request(args[0] || "screen")
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
class WakeLockSentinel
|
|
451
|
+
include EventTarget
|
|
452
|
+
|
|
453
|
+
attr_reader :type
|
|
454
|
+
|
|
455
|
+
def initialize(window, type)
|
|
456
|
+
@window = window
|
|
457
|
+
@type = type
|
|
458
|
+
@released = false
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def released
|
|
462
|
+
@released
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def release
|
|
466
|
+
return PromiseValue.resolve(@window, nil) if @released
|
|
467
|
+
|
|
468
|
+
@released = true
|
|
469
|
+
dispatch_event(Event.new("release"))
|
|
470
|
+
PromiseValue.resolve(@window, nil)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def __js_get__(key)
|
|
474
|
+
case key
|
|
475
|
+
when "type"
|
|
476
|
+
@type
|
|
477
|
+
when "released"
|
|
478
|
+
@released
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def __js_call__(method, _args)
|
|
483
|
+
case method
|
|
484
|
+
when "release"
|
|
485
|
+
release
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def __internal_event_parent__
|
|
490
|
+
nil
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# `navigator.getBattery()` returns one of these. Fixed snapshot —
|
|
495
|
+
# tests that need different values can stub.
|
|
496
|
+
class BatteryManager
|
|
497
|
+
include EventTarget
|
|
498
|
+
|
|
499
|
+
attr_reader :charging, :charging_time, :discharging_time, :level
|
|
500
|
+
|
|
501
|
+
def initialize(charging: true, level: 1.0, charging_time: 0, discharging_time: Float::INFINITY)
|
|
502
|
+
@charging = charging
|
|
503
|
+
@level = level
|
|
504
|
+
@charging_time = charging_time
|
|
505
|
+
@discharging_time = discharging_time
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def __js_get__(key)
|
|
509
|
+
case key
|
|
510
|
+
when "charging"
|
|
511
|
+
@charging
|
|
512
|
+
when "chargingTime"
|
|
513
|
+
@charging_time
|
|
514
|
+
when "dischargingTime"
|
|
515
|
+
@discharging_time
|
|
516
|
+
when "level"
|
|
517
|
+
@level
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def __internal_event_parent__
|
|
268
522
|
nil
|
|
269
523
|
end
|
|
270
524
|
end
|
|
525
|
+
|
|
526
|
+
# `navigator.locks` — Web Locks API. Locks are scoped to the
|
|
527
|
+
# Navigator instance; serial execution per name. Real browsers
|
|
528
|
+
# coordinate across tabs; dommy is single-process so it just
|
|
529
|
+
# serializes calls within the same Window.
|
|
530
|
+
#
|
|
531
|
+
# Spec: https://w3c.github.io/web-locks/
|
|
532
|
+
class LockManager
|
|
533
|
+
def initialize(window)
|
|
534
|
+
@window = window
|
|
535
|
+
@held = {}
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def request(name, options_or_callback, callback = nil)
|
|
539
|
+
if options_or_callback.is_a?(Hash) || options_or_callback.nil?
|
|
540
|
+
options = options_or_callback || {}
|
|
541
|
+
cb = callback
|
|
542
|
+
else
|
|
543
|
+
options = {}
|
|
544
|
+
cb = options_or_callback
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
key = name.to_s
|
|
548
|
+
if @held[key] && options["ifAvailable"]
|
|
549
|
+
return invoke_with_lock(cb, nil)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
lock = Lock.new(key, options["mode"] || "exclusive")
|
|
553
|
+
@held[key] = lock
|
|
554
|
+
result = invoke_with_lock(cb, lock)
|
|
555
|
+
@held.delete(key)
|
|
556
|
+
result
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def query
|
|
560
|
+
held = @held.map { |name, lock| {"name" => name, "mode" => lock.mode, "clientId" => "dommy"} }
|
|
561
|
+
PromiseValue.resolve(@window, {"held" => held, "pending" => []})
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def __js_call__(method, args)
|
|
565
|
+
case method
|
|
566
|
+
when "request"
|
|
567
|
+
request(args[0], args[1], args[2])
|
|
568
|
+
when "query"
|
|
569
|
+
query
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
private
|
|
574
|
+
|
|
575
|
+
def invoke_with_lock(callback, lock)
|
|
576
|
+
value = if callback.respond_to?(:__js_call__)
|
|
577
|
+
callback.__js_call__("call", [lock])
|
|
578
|
+
elsif callback.respond_to?(:call)
|
|
579
|
+
callback.call(lock)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
PromiseValue.resolve(@window, value)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
Lock = Struct.new(:name, :mode) do
|
|
587
|
+
def __js_get__(key)
|
|
588
|
+
case key
|
|
589
|
+
when "name"
|
|
590
|
+
name
|
|
591
|
+
when "mode"
|
|
592
|
+
mode
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# `navigator.storage` — StorageManager API. Returns fixed-value
|
|
598
|
+
# estimates; `persist`/`persisted` always resolve `true`.
|
|
599
|
+
#
|
|
600
|
+
# Spec: https://storage.spec.whatwg.org/
|
|
601
|
+
class StorageManager
|
|
602
|
+
def initialize(window)
|
|
603
|
+
@window = window
|
|
604
|
+
@persisted = false
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def estimate
|
|
608
|
+
PromiseValue.resolve(@window, {"quota" => 1_073_741_824, "usage" => 0, "usageDetails" => {}})
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def persist
|
|
612
|
+
@persisted = true
|
|
613
|
+
PromiseValue.resolve(@window, true)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def persisted
|
|
617
|
+
PromiseValue.resolve(@window, @persisted)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def __js_call__(method, _args)
|
|
621
|
+
case method
|
|
622
|
+
when "estimate"
|
|
623
|
+
estimate
|
|
624
|
+
when "persist"
|
|
625
|
+
persist
|
|
626
|
+
when "persisted"
|
|
627
|
+
persisted
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
end
|
|
271
631
|
end
|
data/lib/dommy/node.rb
CHANGED
|
@@ -10,6 +10,12 @@ module Dommy
|
|
|
10
10
|
# snapshots tree state at the time of the query, matching what
|
|
11
11
|
# most happy-dom test patterns expect.
|
|
12
12
|
class NodeList < Array
|
|
13
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
14
|
+
JS_METHOD_NAMES = %w[item forEach entries keys values].freeze
|
|
15
|
+
def __js_method_names__
|
|
16
|
+
JS_METHOD_NAMES
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
# Spec-compliant: out-of-range returns nil, not raise (Array#[] is
|
|
14
20
|
# close but we make negative indices fail too — DOM `item(-1)` is
|
|
15
21
|
# nil, not Array#[-1]'s last element).
|
|
@@ -88,6 +94,12 @@ module Dommy
|
|
|
88
94
|
class LiveNodeList
|
|
89
95
|
include Enumerable
|
|
90
96
|
|
|
97
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
98
|
+
JS_METHOD_NAMES = %w[item forEach entries keys values].freeze
|
|
99
|
+
def __js_method_names__
|
|
100
|
+
JS_METHOD_NAMES
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
def initialize(&block)
|
|
92
104
|
@compute = block
|
|
93
105
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `Notification` polyfill. Real browsers prompt the user; dommy
|
|
5
|
+
# exposes the permission state as a class-level slot tests can
|
|
6
|
+
# toggle via `Notification.__test_set_permission__("granted")`.
|
|
7
|
+
#
|
|
8
|
+
# Spec: https://notifications.spec.whatwg.org/
|
|
9
|
+
class Notification
|
|
10
|
+
include EventTarget
|
|
11
|
+
|
|
12
|
+
@permission = "default"
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_reader :permission
|
|
16
|
+
|
|
17
|
+
def __test_set_permission__(value)
|
|
18
|
+
@permission = value.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Asynchronous spec API: returns a Promise (here a value that
|
|
22
|
+
# `.await`-able). Callbacks receive the current permission
|
|
23
|
+
# value.
|
|
24
|
+
def request_permission(window, callback = nil)
|
|
25
|
+
promise = PromiseValue.resolve(window, @permission)
|
|
26
|
+
if callback.respond_to?(:__js_call__)
|
|
27
|
+
callback.__js_call__("call", [@permission])
|
|
28
|
+
elsif callback.respond_to?(:call)
|
|
29
|
+
callback.call(@permission)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
promise
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :title, :body, :icon, :tag, :data
|
|
37
|
+
|
|
38
|
+
def initialize(window, title, options = nil)
|
|
39
|
+
@window = window
|
|
40
|
+
@title = title.to_s
|
|
41
|
+
opts = options.is_a?(Hash) ? options : {}
|
|
42
|
+
@body = (opts["body"] || opts[:body] || "").to_s
|
|
43
|
+
@icon = (opts["icon"] || opts[:icon] || "").to_s
|
|
44
|
+
@tag = (opts["tag"] || opts[:tag] || "").to_s
|
|
45
|
+
@data = opts["data"] || opts[:data]
|
|
46
|
+
@closed = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
return if @closed
|
|
51
|
+
|
|
52
|
+
@closed = true
|
|
53
|
+
dispatch_event(Event.new("close"))
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def __js_get__(key)
|
|
58
|
+
case key
|
|
59
|
+
when "title"
|
|
60
|
+
@title
|
|
61
|
+
when "body"
|
|
62
|
+
@body
|
|
63
|
+
when "icon"
|
|
64
|
+
@icon
|
|
65
|
+
when "tag"
|
|
66
|
+
@tag
|
|
67
|
+
when "data"
|
|
68
|
+
@data
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def __js_call__(method, args)
|
|
73
|
+
case method
|
|
74
|
+
when "close"
|
|
75
|
+
close
|
|
76
|
+
when "addEventListener"
|
|
77
|
+
add_event_listener(args[0], args[1], args[2])
|
|
78
|
+
when "removeEventListener"
|
|
79
|
+
remove_event_listener(args[0], args[1])
|
|
80
|
+
when "dispatchEvent"
|
|
81
|
+
dispatch_event(args[0])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def __internal_event_parent__
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/dommy/parser.rb
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
|
-
|
|
5
3
|
module Dommy
|
|
6
|
-
# Thin wrapper around
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# Thin wrapper around the backend's HTML5 fragment parser. Delegates
|
|
5
|
+
# to `Dommy::Backend.fragment` so backends can supply their own
|
|
6
|
+
# implementation.
|
|
9
7
|
#
|
|
10
|
-
# Known quirks
|
|
11
|
-
#
|
|
12
|
-
#
|
|
8
|
+
# Known quirks (vary by backend):
|
|
9
|
+
# - Nokogiri (libxml2): `<table>`-only fragments wrap children in
|
|
10
|
+
# an implicit `<tbody>`; `<select>` reparents non-option children.
|
|
11
|
+
# - Nokolexbor (Lexbor): similar behavior, slightly different edge
|
|
12
|
+
# cases for malformed input.
|
|
13
13
|
#
|
|
14
14
|
# `owner_doc` is critical: when a node parsed via a detached
|
|
15
15
|
# fragment gets `add_child`'d into a Document with a different
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# owner, libxml2 silently **copies** the node (new object_id)
|
|
17
|
+
# instead of moving it. That breaks identity-dependent caches
|
|
18
|
+
# (e.g. `Document#wrap_node` and any reconciler that keys off
|
|
19
|
+
# node identity). Always pass the destination document.
|
|
20
20
|
module Parser
|
|
21
21
|
def self.fragment(html, owner_doc: nil)
|
|
22
22
|
if owner_doc
|
|
23
23
|
owner_doc.fragment(html.to_s)
|
|
24
24
|
else
|
|
25
|
-
|
|
25
|
+
Backend.fragment(html.to_s, owner_doc: nil)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `window.performance` — User Timing API (mark / measure) plus a
|
|
5
|
+
# virtual `now` clock backed by the deterministic scheduler.
|
|
6
|
+
#
|
|
7
|
+
# Spec:
|
|
8
|
+
# - User Timing: https://www.w3.org/TR/user-timing/
|
|
9
|
+
# - HRT (now): https://www.w3.org/TR/hr-time/
|
|
10
|
+
class Performance
|
|
11
|
+
def initialize(window)
|
|
12
|
+
@window = window
|
|
13
|
+
@entries = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def now
|
|
17
|
+
@window.scheduler.now_ms.to_f
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def mark(name, options = nil)
|
|
21
|
+
start_time = options.is_a?(Hash) && options.key?("startTime") ? options["startTime"].to_f : now
|
|
22
|
+
detail = options.is_a?(Hash) ? options["detail"] : nil
|
|
23
|
+
entry = PerformanceEntry.new(
|
|
24
|
+
name: name.to_s,
|
|
25
|
+
entry_type: "mark",
|
|
26
|
+
start_time: start_time,
|
|
27
|
+
duration: 0.0,
|
|
28
|
+
detail: detail
|
|
29
|
+
)
|
|
30
|
+
@entries << entry
|
|
31
|
+
entry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def measure(name, start_or_options = nil, end_mark = nil)
|
|
35
|
+
if start_or_options.is_a?(Hash)
|
|
36
|
+
start = resolve_time(start_or_options["start"])
|
|
37
|
+
finish = resolve_time(start_or_options["end"])
|
|
38
|
+
else
|
|
39
|
+
start = resolve_time(start_or_options)
|
|
40
|
+
finish = resolve_time(end_mark)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
start ||= 0.0
|
|
44
|
+
finish ||= now
|
|
45
|
+
entry = PerformanceEntry.new(
|
|
46
|
+
name: name.to_s,
|
|
47
|
+
entry_type: "measure",
|
|
48
|
+
start_time: start,
|
|
49
|
+
duration: finish - start
|
|
50
|
+
)
|
|
51
|
+
@entries << entry
|
|
52
|
+
entry
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear_marks(name = nil)
|
|
56
|
+
@entries.reject! { |e| e.entry_type == "mark" && (name.nil? || e.name == name.to_s) }
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias clearMarks clear_marks
|
|
61
|
+
|
|
62
|
+
def clear_measures(name = nil)
|
|
63
|
+
@entries.reject! { |e| e.entry_type == "measure" && (name.nil? || e.name == name.to_s) }
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
alias clearMeasures clear_measures
|
|
68
|
+
|
|
69
|
+
def get_entries
|
|
70
|
+
@entries.dup
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
alias getEntries get_entries
|
|
74
|
+
|
|
75
|
+
def get_entries_by_name(name, entry_type = nil)
|
|
76
|
+
@entries.select { |e| e.name == name.to_s && (entry_type.nil? || e.entry_type == entry_type.to_s) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
alias getEntriesByName get_entries_by_name
|
|
80
|
+
|
|
81
|
+
def get_entries_by_type(entry_type)
|
|
82
|
+
@entries.select { |e| e.entry_type == entry_type.to_s }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
alias getEntriesByType get_entries_by_type
|
|
86
|
+
|
|
87
|
+
def __js_get__(key)
|
|
88
|
+
case key
|
|
89
|
+
when "now"
|
|
90
|
+
now
|
|
91
|
+
when "timeOrigin"
|
|
92
|
+
0.0
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def __js_call__(method, args)
|
|
97
|
+
case method
|
|
98
|
+
when "now"
|
|
99
|
+
now
|
|
100
|
+
when "mark"
|
|
101
|
+
mark(args[0], args[1])
|
|
102
|
+
when "measure"
|
|
103
|
+
measure(args[0], args[1], args[2])
|
|
104
|
+
when "clearMarks"
|
|
105
|
+
clear_marks(args[0])
|
|
106
|
+
when "clearMeasures"
|
|
107
|
+
clear_measures(args[0])
|
|
108
|
+
when "getEntries"
|
|
109
|
+
get_entries
|
|
110
|
+
when "getEntriesByName"
|
|
111
|
+
get_entries_by_name(args[0], args[1])
|
|
112
|
+
when "getEntriesByType"
|
|
113
|
+
get_entries_by_type(args[0])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Resolve a `mark name` or numeric timestamp to a `now`-relative time.
|
|
120
|
+
def resolve_time(value)
|
|
121
|
+
return nil if value.nil?
|
|
122
|
+
return value.to_f if value.is_a?(Numeric)
|
|
123
|
+
|
|
124
|
+
mark = @entries.reverse.find { |e| e.entry_type == "mark" && e.name == value.to_s }
|
|
125
|
+
mark ? mark.start_time : nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `PerformanceEntry` — common shape for User Timing marks/measures.
|
|
130
|
+
PerformanceEntry = Struct.new(:name, :entry_type, :start_time, :duration, :detail, keyword_init: true) do
|
|
131
|
+
def __js_get__(key)
|
|
132
|
+
case key
|
|
133
|
+
when "name"
|
|
134
|
+
name
|
|
135
|
+
when "entryType"
|
|
136
|
+
entry_type
|
|
137
|
+
when "startTime"
|
|
138
|
+
start_time
|
|
139
|
+
when "duration"
|
|
140
|
+
duration
|
|
141
|
+
when "detail"
|
|
142
|
+
detail
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|