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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -13
  3. data/lib/dommy/animation.rb +288 -0
  4. data/lib/dommy/attr.rb +23 -11
  5. data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
  6. data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
  7. data/lib/dommy/backend.rb +129 -0
  8. data/lib/dommy/blob.rb +2 -2
  9. data/lib/dommy/compression_streams.rb +147 -0
  10. data/lib/dommy/cookie_store.rb +128 -0
  11. data/lib/dommy/crypto.rb +396 -0
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +190 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +356 -53
  17. data/lib/dommy/event.rb +431 -25
  18. data/lib/dommy/event_source.rb +131 -0
  19. data/lib/dommy/fetch.rb +76 -6
  20. data/lib/dommy/file_reader.rb +176 -0
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +82 -0
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +130 -67
  25. data/lib/dommy/internal/cookie_jar.rb +2 -0
  26. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  27. data/lib/dommy/internal/dom_matching.rb +4 -4
  28. data/lib/dommy/internal/idna.rb +443 -0
  29. data/lib/dommy/internal/idna_data.rb +10379 -0
  30. data/lib/dommy/internal/ipv4_parser.rb +78 -0
  31. data/lib/dommy/internal/node_traversal.rb +1 -1
  32. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  33. data/lib/dommy/internal/observable_callback.rb +25 -0
  34. data/lib/dommy/internal/punycode.rb +202 -0
  35. data/lib/dommy/internal/range_text_serializer.rb +72 -0
  36. data/lib/dommy/internal/reflected_attributes.rb +45 -0
  37. data/lib/dommy/internal/template_content_registry.rb +6 -6
  38. data/lib/dommy/intersection_observer.rb +82 -0
  39. data/lib/dommy/{router.rb → location.rb} +8 -142
  40. data/lib/dommy/media_query_list.rb +118 -0
  41. data/lib/dommy/message_channel.rb +249 -0
  42. data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
  43. data/lib/dommy/navigator.rb +365 -5
  44. data/lib/dommy/node.rb +12 -0
  45. data/lib/dommy/notification.rb +89 -0
  46. data/lib/dommy/parser.rb +13 -13
  47. data/lib/dommy/performance.rb +146 -0
  48. data/lib/dommy/performance_observer.rb +55 -0
  49. data/lib/dommy/range.rb +597 -0
  50. data/lib/dommy/resize_observer.rb +53 -0
  51. data/lib/dommy/shadow_root.rb +10 -8
  52. data/lib/dommy/streams.rb +386 -0
  53. data/lib/dommy/svg_elements.rb +3863 -0
  54. data/lib/dommy/text_codec.rb +175 -0
  55. data/lib/dommy/tree_walker.rb +21 -21
  56. data/lib/dommy/url.rb +274 -29
  57. data/lib/dommy/url_pattern.rb +144 -0
  58. data/lib/dommy/version.rb +1 -1
  59. data/lib/dommy/web_socket.rb +209 -0
  60. data/lib/dommy/window.rb +369 -0
  61. data/lib/dommy/worker.rb +143 -0
  62. data/lib/dommy/xml_http_request.rb +438 -0
  63. data/lib/dommy.rb +43 -5
  64. metadata +44 -29
  65. data/lib/dommy/world.rb +0 -209
@@ -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 __event_parent__
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&.__set_state__(state.to_s)
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 __set_state__(new_state)
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 __event_parent__
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 Nokogiri's HTML5 fragment parser. Pinned to
7
- # `max_errors: 0` for silent recovery on malformed HTML (matching
8
- # browser behavior).
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: `<table>`-only fragments wrap children in an
11
- # implicit `<tbody>`; `<select>` reparents non-option children
12
- # outside itself.
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
- # Nokogiri owner, libxml2 silently **copies** the node (new
17
- # object_id) instead of moving it. That breaks identity-dependent
18
- # caches (e.g. `Document#wrap_node` and any reconciler that keys
19
- # off node identity). Always pass the destination document.
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
- Nokogiri::HTML5.fragment(html.to_s, max_errors: 0)
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