dommy 0.8.0 → 0.9.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/dommy/animation.rb +4 -0
  4. data/lib/dommy/attr.rb +11 -5
  5. data/lib/dommy/backend/makiri_adapter.rb +330 -0
  6. data/lib/dommy/backend.rb +114 -33
  7. data/lib/dommy/blob.rb +2 -0
  8. data/lib/dommy/bridge.rb +11 -0
  9. data/lib/dommy/browser.rb +217 -0
  10. data/lib/dommy/compression_streams.rb +4 -0
  11. data/lib/dommy/crypto.rb +4 -0
  12. data/lib/dommy/css.rb +487 -50
  13. data/lib/dommy/custom_elements.rb +2 -2
  14. data/lib/dommy/data_transfer.rb +2 -0
  15. data/lib/dommy/data_uri.rb +35 -0
  16. data/lib/dommy/deferred_response.rb +59 -0
  17. data/lib/dommy/document.rb +386 -228
  18. data/lib/dommy/dom_exception.rb +2 -0
  19. data/lib/dommy/dom_parser.rb +7 -17
  20. data/lib/dommy/element.rb +502 -155
  21. data/lib/dommy/event.rb +240 -9
  22. data/lib/dommy/fetch.rb +152 -34
  23. data/lib/dommy/form_data.rb +2 -0
  24. data/lib/dommy/history.rb +2 -0
  25. data/lib/dommy/html_canvas_element.rb +230 -0
  26. data/lib/dommy/html_collection.rb +5 -6
  27. data/lib/dommy/html_elements.rb +304 -27
  28. data/lib/dommy/interaction/debug.rb +35 -0
  29. data/lib/dommy/interaction/dom_summary.rb +131 -0
  30. data/lib/dommy/interaction/driver.rb +244 -0
  31. data/lib/dommy/interaction/event_synthesis.rb +56 -0
  32. data/lib/dommy/interaction/field_interactor.rb +117 -0
  33. data/lib/dommy/interaction/form_submission.rb +268 -0
  34. data/lib/dommy/interaction/locator.rb +158 -0
  35. data/lib/dommy/interaction/role_query.rb +58 -0
  36. data/lib/dommy/interaction.rb +32 -0
  37. data/lib/dommy/internal/accessibility_tree.rb +215 -0
  38. data/lib/dommy/internal/accessible_description.rb +38 -0
  39. data/lib/dommy/internal/accessible_name.rb +301 -0
  40. data/lib/dommy/internal/aria_role.rb +252 -0
  41. data/lib/dommy/internal/aria_snapshot.rb +64 -0
  42. data/lib/dommy/internal/aria_state.rb +151 -0
  43. data/lib/dommy/internal/css/calc.rb +242 -0
  44. data/lib/dommy/internal/css/cascade.rb +430 -0
  45. data/lib/dommy/internal/css/color.rb +381 -0
  46. data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
  47. data/lib/dommy/internal/css/counters.rb +227 -0
  48. data/lib/dommy/internal/css/custom_properties.rb +183 -0
  49. data/lib/dommy/internal/css/media_query.rb +302 -0
  50. data/lib/dommy/internal/css/parser.rb +265 -0
  51. data/lib/dommy/internal/css/property_registry.rb +512 -0
  52. data/lib/dommy/internal/css/rule_index.rb +494 -0
  53. data/lib/dommy/internal/css/supports.rb +158 -0
  54. data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
  55. data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
  56. data/lib/dommy/internal/css_rule_text.rb +160 -0
  57. data/lib/dommy/internal/dom_matching.rb +80 -9
  58. data/lib/dommy/internal/element_matching.rb +109 -0
  59. data/lib/dommy/internal/global_functions.rb +33 -0
  60. data/lib/dommy/internal/mutation_coordinator.rb +95 -4
  61. data/lib/dommy/internal/namespaces.rb +49 -5
  62. data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
  63. data/lib/dommy/internal/parent_node.rb +82 -5
  64. data/lib/dommy/internal/selector_ast.rb +124 -0
  65. data/lib/dommy/internal/selector_index.rb +146 -0
  66. data/lib/dommy/internal/selector_matcher.rb +756 -0
  67. data/lib/dommy/internal/selector_parser.rb +283 -131
  68. data/lib/dommy/internal/shadow_root_registry.rb +9 -2
  69. data/lib/dommy/internal/template_content_registry.rb +26 -18
  70. data/lib/dommy/internal/xml_serialization.rb +344 -0
  71. data/lib/dommy/intersection_observer.rb +2 -0
  72. data/lib/dommy/js/bridge_conformance.rb +80 -0
  73. data/lib/dommy/js/constructor_resolver.rb +44 -0
  74. data/lib/dommy/js/custom_element_bridge.rb +90 -0
  75. data/lib/dommy/js/dom_interfaces.rb +162 -0
  76. data/lib/dommy/js/handle_table.rb +60 -0
  77. data/lib/dommy/js/host_bridge.rb +517 -0
  78. data/lib/dommy/js/host_runtime.js +1495 -0
  79. data/lib/dommy/js/import_map.rb +58 -0
  80. data/lib/dommy/js/marshaller.rb +240 -0
  81. data/lib/dommy/js/module_loader.rb +99 -0
  82. data/lib/dommy/js/observable_runtime.js +742 -0
  83. data/lib/dommy/js/runtime.rb +115 -0
  84. data/lib/dommy/js/script_boot.rb +221 -0
  85. data/lib/dommy/js/wire_tags.rb +62 -0
  86. data/lib/dommy/location.rb +2 -0
  87. data/lib/dommy/media_query_list.rb +50 -14
  88. data/lib/dommy/message_channel.rb +22 -6
  89. data/lib/dommy/minitest/assertions.rb +27 -0
  90. data/lib/dommy/mutation_observer.rb +89 -4
  91. data/lib/dommy/navigator.rb +34 -2
  92. data/lib/dommy/node.rb +24 -14
  93. data/lib/dommy/notification.rb +2 -0
  94. data/lib/dommy/parser.rb +1 -1
  95. data/lib/dommy/performance.rb +21 -1
  96. data/lib/dommy/promise.rb +94 -10
  97. data/lib/dommy/range.rb +173 -31
  98. data/lib/dommy/resources.rb +178 -0
  99. data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
  100. data/lib/dommy/scheduler.rb +149 -13
  101. data/lib/dommy/screen.rb +91 -0
  102. data/lib/dommy/shadow_root.rb +76 -13
  103. data/lib/dommy/storage.rb +2 -1
  104. data/lib/dommy/streams.rb +6 -0
  105. data/lib/dommy/text_codec.rb +7 -1
  106. data/lib/dommy/tree_walker.rb +33 -10
  107. data/lib/dommy/url.rb +13 -1
  108. data/lib/dommy/version.rb +1 -1
  109. data/lib/dommy/window.rb +199 -11
  110. data/lib/dommy/worker.rb +8 -4
  111. data/lib/dommy/xml_http_request.rb +47 -6
  112. data/lib/dommy.rb +36 -1
  113. metadata +96 -10
  114. data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
  115. data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
data/lib/dommy/history.rb CHANGED
@@ -25,6 +25,8 @@ module Dommy
25
25
  @stack[@cursor][:state]
26
26
  when "scrollRestoration"
27
27
  @scroll_restoration
28
+ else
29
+ Bridge::ABSENT
28
30
  end
29
31
  end
30
32
 
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `<canvas>` — Dommy has no raster backend, so the API surface is implemented
5
+ # as inert stubs: `getContext('2d')` returns a CanvasRenderingContext2D whose
6
+ # draw operations are no-ops and whose read-backs are zeroed. The point is not
7
+ # to draw but to keep the *many* sites that merely touch a canvas from
8
+ # crashing: sprite/asset loaders, "am I a bot" canvas fingerprints, and chart
9
+ # libraries that feature-detect 2D support all call `getContext` then
10
+ # `fillRect` / `createImageData` / `measureText`. With no canvas element those
11
+ # are `undefined`, so the call throws "getContext is not a function" and aborts
12
+ # the whole bundle — exactly what hatena's bookmark.js does, which silently
13
+ # broke the page's bookmark button. WebGL is reported as genuinely unsupported
14
+ # (getContext returns null) so callers take their fallback path.
15
+ class HTMLCanvasElement < HTMLElement
16
+ DEFAULT_WIDTH = 300
17
+ DEFAULT_HEIGHT = 150
18
+
19
+ # A 1x1 transparent PNG — a constant so a canvas fingerprint reads a stable
20
+ # value instead of crashing; we render nothing into it.
21
+ BLANK_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lE" \
22
+ "QVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
23
+
24
+ def width = int_dimension("width", DEFAULT_WIDTH)
25
+ def height = int_dimension("height", DEFAULT_HEIGHT)
26
+
27
+ def width=(value)
28
+ set_reflected_string("width", value.to_s)
29
+ end
30
+
31
+ def height=(value)
32
+ set_reflected_string("height", value.to_s)
33
+ end
34
+
35
+ # Spec: getContext returns the SAME object across calls for one context id.
36
+ # Only '2d' is backed; webgl/webgl2/bitmaprenderer return null so feature
37
+ # detection cleanly fails over.
38
+ def get_context(context_id, *_options)
39
+ return nil unless context_id.to_s == "2d"
40
+
41
+ @__context_2d ||= CanvasRenderingContext2D.new(self)
42
+ end
43
+
44
+ def to_data_url(*_args) = BLANK_PNG
45
+
46
+ def to_blob(callback = nil, *_args)
47
+ return nil unless callback.respond_to?(:call)
48
+
49
+ callback.call(Blob.new([], {"type" => "image/png"}))
50
+ nil
51
+ end
52
+
53
+ def __js_get__(key)
54
+ case key
55
+ when "width" then width
56
+ when "height" then height
57
+ else super
58
+ end
59
+ end
60
+
61
+ def __js_set__(key, value)
62
+ case key
63
+ when "width", "height" then set_reflected_string(key, value.to_s)
64
+ else super
65
+ end
66
+ end
67
+
68
+ include Bridge::Methods
69
+ js_methods %w[getContext toDataURL toBlob]
70
+ def __js_call__(method, args)
71
+ case method
72
+ when "getContext" then get_context(args[0], *args[1..])
73
+ when "toDataURL" then to_data_url(*args)
74
+ when "toBlob" then to_blob(*args)
75
+ else super
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def int_dimension(attr, default)
82
+ raw = @__node__[attr]
83
+ raw.nil? || raw.to_s.empty? ? default : raw.to_s.to_i
84
+ end
85
+ end
86
+
87
+ # The 2D drawing context — every drawing call is a no-op, state setters are
88
+ # remembered (so `ctx.fillStyle` round-trips), and read-backs return zeroed
89
+ # data of the right shape. Enough for asset loaders / fingerprint probes /
90
+ # chart feature-detection to run without throwing.
91
+ class CanvasRenderingContext2D
92
+ # Drawing-state attributes that round-trip a value (default per spec where it
93
+ # matters; a plain "" otherwise).
94
+ STATE_DEFAULTS = {
95
+ "fillStyle" => "#000000", "strokeStyle" => "#000000",
96
+ "globalAlpha" => 1.0, "globalCompositeOperation" => "source-over",
97
+ "lineWidth" => 1.0, "lineCap" => "butt", "lineJoin" => "miter", "miterLimit" => 10.0,
98
+ "lineDashOffset" => 0.0, "font" => "10px sans-serif", "textAlign" => "start",
99
+ "textBaseline" => "alphabetic", "direction" => "inherit",
100
+ "shadowBlur" => 0.0, "shadowColor" => "rgba(0, 0, 0, 0)",
101
+ "shadowOffsetX" => 0.0, "shadowOffsetY" => 0.0,
102
+ "imageSmoothingEnabled" => true, "imageSmoothingQuality" => "low", "filter" => "none"
103
+ }.freeze
104
+
105
+ def initialize(canvas)
106
+ @canvas = canvas
107
+ @state = STATE_DEFAULTS.dup
108
+ end
109
+
110
+ def __js_get__(key)
111
+ return @canvas if key == "canvas"
112
+ return @state[key] if @state.key?(key)
113
+
114
+ Bridge::ABSENT
115
+ end
116
+
117
+ def __js_set__(key, value)
118
+ return Bridge::UNHANDLED unless @state.key?(key)
119
+
120
+ @state[key] = value
121
+ value
122
+ end
123
+
124
+ include Bridge::Methods
125
+ js_methods %w[
126
+ save restore scale rotate translate transform setTransform resetTransform getTransform
127
+ beginPath closePath moveTo lineTo bezierCurveTo quadraticCurveTo arc arcTo ellipse rect roundRect
128
+ fill stroke clip fillRect strokeRect clearRect fillText strokeText drawImage putImageData
129
+ setLineDash drawFocusIfNeeded scrollPathIntoView reset
130
+ getLineDash measureText createLinearGradient createRadialGradient createConicGradient
131
+ createPattern getImageData createImageData isPointInPath isPointInStroke getContextAttributes
132
+ ]
133
+ def __js_call__(method, args)
134
+ case method
135
+ # All drawing/state-machine operations: nothing is painted.
136
+ when "save", "restore", "scale", "rotate", "translate", "transform", "setTransform",
137
+ "resetTransform", "getTransform", "beginPath", "closePath", "moveTo", "lineTo",
138
+ "bezierCurveTo", "quadraticCurveTo", "arc", "arcTo", "ellipse", "rect", "roundRect",
139
+ "fill", "stroke", "clip", "fillRect", "strokeRect", "clearRect", "fillText", "strokeText",
140
+ "drawImage", "putImageData", "setLineDash", "drawFocusIfNeeded", "scrollPathIntoView", "reset"
141
+ nil
142
+ when "getLineDash" then []
143
+ when "measureText" then TextMetrics.new(args[0].to_s)
144
+ when "createLinearGradient", "createRadialGradient", "createConicGradient", "createPattern"
145
+ CanvasGradient.new
146
+ when "getImageData" then ImageData.new(args[2].to_i.abs, args[3].to_i.abs)
147
+ when "createImageData" then created_image_data(args)
148
+ when "isPointInPath", "isPointInStroke" then false
149
+ when "getContextAttributes" then {}
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # createImageData(imagedata) clones its dimensions; createImageData(w, h)
156
+ # builds a blank one. A zero/blank size still yields a valid 0-length buffer.
157
+ def created_image_data(args)
158
+ first = args[0]
159
+ if first.respond_to?(:__js_get__) && !first.__js_get__("width").equal?(Bridge::ABSENT)
160
+ ImageData.new(first.__js_get__("width").to_i, first.__js_get__("height").to_i)
161
+ else
162
+ ImageData.new(first.to_i.abs, args[1].to_i.abs)
163
+ end
164
+ end
165
+ end
166
+
167
+ # `ctx.measureText(...)` result. width is an approximation (no font metrics);
168
+ # the extended box metrics are zero. Enough that callers reading `.width`
169
+ # don't divide by undefined.
170
+ class TextMetrics
171
+ APPROX_CHAR_WIDTH = 6
172
+
173
+ def initialize(text)
174
+ @width = text.to_s.length * APPROX_CHAR_WIDTH
175
+ end
176
+
177
+ def __js_get__(key)
178
+ case key
179
+ when "width" then @width
180
+ when "actualBoundingBoxLeft", "actualBoundingBoxRight",
181
+ "actualBoundingBoxAscent", "actualBoundingBoxDescent",
182
+ "fontBoundingBoxAscent", "fontBoundingBoxDescent",
183
+ "emHeightAscent", "emHeightDescent",
184
+ "hangingBaseline", "alphabeticBaseline", "ideographicBaseline" then 0
185
+ else Bridge::ABSENT
186
+ end
187
+ end
188
+
189
+ def __js_set__(_key, _value) = Bridge::UNHANDLED
190
+ end
191
+
192
+ # A gradient/pattern handle. addColorStop is a no-op (nothing is painted).
193
+ class CanvasGradient
194
+ def __js_get__(_key) = Bridge::ABSENT
195
+ def __js_set__(_key, _value) = Bridge::UNHANDLED
196
+
197
+ include Bridge::Methods
198
+ js_methods %w[addColorStop setTransform]
199
+ def __js_call__(method, _args)
200
+ case method
201
+ when "addColorStop", "setTransform" then nil
202
+ end
203
+ end
204
+ end
205
+
206
+ # `ctx.getImageData(...)` / `createImageData(...)` result: width, height, and a
207
+ # zeroed RGBA `data` buffer of length width*height*4 (a plain numeric array,
208
+ # which supports the `.length` and `data[i]` reads canvas code does).
209
+ class ImageData
210
+ attr_reader :width, :height
211
+
212
+ def initialize(width, height)
213
+ @width = [width.to_i, 0].max
214
+ @height = [height.to_i, 0].max
215
+ @data = Array.new(@width * @height * 4, 0)
216
+ end
217
+
218
+ def __js_get__(key)
219
+ case key
220
+ when "width" then @width
221
+ when "height" then @height
222
+ when "data" then @data
223
+ when "colorSpace" then "srgb"
224
+ else Bridge::ABSENT
225
+ end
226
+ end
227
+
228
+ def __js_set__(_key, _value) = Bridge::UNHANDLED
229
+ end
230
+ end
@@ -52,10 +52,9 @@ module Dommy
52
52
  end
53
53
 
54
54
  def item(index)
55
- i = index.to_i
56
- return nil if i < 0
57
-
58
- to_a[i]
55
+ # `index` is a WebIDL unsigned long, so it wraps modulo 2^32 (e.g. item(2^32)
56
+ # is item(0)); Ruby's modulo also normalizes negatives to that range.
57
+ to_a[index.to_i % 4_294_967_296]
59
58
  end
60
59
 
61
60
  # `namedItem(name)` returns the first element whose `id` or
@@ -119,8 +118,8 @@ module Dommy
119
118
  item(s.to_i)
120
119
  else
121
120
  # Non-array-index strings (negative, ≥ 2^32-1, or names) use the named
122
- # getter.
123
- named_item(s) || (s == "length" ? length : nil)
121
+ # getter; a miss is JS `undefined` (and `"x" in coll` false).
122
+ named_item(s) || (s == "length" ? length : Bridge::ABSENT)
124
123
  end
125
124
  end
126
125
  end