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
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # A lightweight test browser: parse HTML, build window/document, run its
5
+ # classic `<script>` tags (inline + external via a resources adapter), fire
6
+ # DOMContentLoaded/load, and collect JS errors / console output. For
7
+ # standalone HTML + JS (bundled SPA, fixture HTML); the Rack/Rails entry point
8
+ # is `Dommy::Rack::Session` (a later phase).
9
+ #
10
+ # Dommy::Browser.open(html, resources: Dommy::Resources.static("/app.js" => "...")) do |b|
11
+ # b.settle
12
+ # b.evaluate('document.querySelector("h1").textContent')
13
+ # end
14
+ #
15
+ # JS errors are not swallowed: in strict mode (default) any unhandled rejection
16
+ # or uncaught script error fails at the next checkpoint (after boot, after
17
+ # `settle`, at dispose). Wrap intentional errors in `allow_js_errors { … }`.
18
+ class Browser
19
+ # Capybara-vocabulary finding / scoping / field interaction / click /
20
+ # matchers come from the shared interaction layer; each interaction's events
21
+ # are dispatched Ruby-side (synchronously invoking JS handlers), then
22
+ # `after_interaction` drains the runtime's microtasks so promise reactions
23
+ # settle before the next line.
24
+ include Dommy::Interaction::Driver
25
+
26
+ # Raised in strict mode when JS errors were collected and not acknowledged.
27
+ class JsError < StandardError
28
+ attr_reader :causes
29
+
30
+ def initialize(causes)
31
+ @causes = causes
32
+ super(build_message(causes))
33
+ end
34
+
35
+ private
36
+
37
+ def build_message(causes)
38
+ lines = causes.map { |e| " #{e.class}: #{e.message}" }
39
+ "#{causes.length} uncaught JS error(s):\n#{lines.join("\n")}"
40
+ end
41
+ end
42
+
43
+ attr_reader :window, :runtime, :js_errors, :console
44
+
45
+ # Build a browser and (unless `execute_scripts: false`) boot its scripts. In
46
+ # block form the browser is yielded and disposed afterward, returning the
47
+ # block value.
48
+ def self.open(html, **opts)
49
+ browser = new(html, **opts)
50
+ return browser unless block_given?
51
+
52
+ begin
53
+ yield browser
54
+ ensure
55
+ browser.dispose
56
+ end
57
+ end
58
+
59
+ def initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true,
60
+ wasm_memory_shim: false, backend: nil)
61
+ @resources = resources
62
+ @strict = strict
63
+ @js_errors = []
64
+ @console = []
65
+ @acknowledged = 0
66
+ @allow_errors = false
67
+ @disposed = false
68
+
69
+ @window = Dommy.parse(html)
70
+ @window.location.__internal_set_url__(url) if url
71
+
72
+ # The JS engine is pluggable: `backend:` selects a registered runtime
73
+ # (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
74
+ @runtime = Js.build_runtime(backend)
75
+ @runtime.on_unhandled_rejection { |err| @js_errors << err }
76
+ @runtime.on_callback_error { |err| @js_errors << err } if @runtime.respond_to?(:on_callback_error)
77
+ @runtime.on_log { |log| @console << log }
78
+ @runtime.define_host_object("document", @window.document)
79
+ @runtime.install_window(@window)
80
+ @runtime.install_browser_globals
81
+ # Opt-in WPT scaffolding (common/sab.js derives SharedArrayBuffer through
82
+ # WebAssembly.Memory); off by default so real pages don't see the shim.
83
+ @runtime.install_wasm_memory_shim if wasm_memory_shim && @runtime.respond_to?(:install_wasm_memory_shim)
84
+ @window.globals["__fetch_handler__"] = Resources::FetchHandler.new(@resources) if @resources
85
+
86
+ if execute_scripts
87
+ doc = @window.document
88
+ # Dynamically-inserted `<script src>` (webpack/Vite on-demand chunks)
89
+ # fetch + run through the same resources adapter, after boot.
90
+ doc.external_script_runner = lambda do |element, src|
91
+ Js::ScriptBoot.run_external_script(@runtime, doc, element, src,
92
+ resources: @resources, on_error: ->(e) { @js_errors << e })
93
+ end
94
+ Js::ScriptBoot.run_document_scripts(
95
+ @runtime, doc, resources: @resources, on_error: ->(e) { @js_errors << e }
96
+ )
97
+ # Leave the page in a ready state: run on-load promises, due-now timers,
98
+ # and rAF (not future timers). `settle: false` observes it mid-flight.
99
+ @runtime.settle if settle
100
+ end
101
+ check_js_errors!
102
+ end
103
+
104
+ def document = @window.document
105
+
106
+ # Current document HTML (serialized).
107
+ def html = @window.document.document_element&.outer_html
108
+
109
+ # Evaluate an expression / statement body and return the decoded value.
110
+ def evaluate(js)
111
+ result = @runtime.evaluate(js)
112
+ check_js_errors!
113
+ result
114
+ end
115
+
116
+ # Run JS for side effects.
117
+ def execute(js)
118
+ @runtime.execute(js)
119
+ check_js_errors!
120
+ nil
121
+ end
122
+
123
+ # Settle the work ready at the current virtual time: drain microtasks, run
124
+ # due-now timers, flush requestAnimationFrame. Does NOT fire a future
125
+ # `setTimeout(300)` — use `advance_time(300)` for debounce/throttle.
126
+ def settle
127
+ @runtime.settle
128
+ check_js_errors!
129
+ self
130
+ end
131
+
132
+ # Advance virtual time by `ms`, running timers that come due, then settle.
133
+ def advance_time(ms)
134
+ @window.scheduler.advance_time(ms)
135
+ @runtime.drain_microtasks
136
+ check_js_errors!
137
+ self
138
+ end
139
+
140
+ # An interaction's events have been dispatched (Ruby-side, synchronously
141
+ # invoking JS handlers); drain the runtime's microtasks so promise reactions
142
+ # land before the next line, then enforce strict mode.
143
+ def after_interaction
144
+ @runtime.drain_microtasks
145
+ check_js_errors!
146
+ end
147
+
148
+ # Click a submit-capable button. The button's click event fires (JS may
149
+ # handle / preventDefault it); if it is an un-prevented submit button, the
150
+ # form's `submit` event is dispatched too (a SPA's JS handles it). Real
151
+ # navigation on an un-prevented submit is a Session concern (out of scope).
152
+ def click_button(locator)
153
+ button = finder.find_button(locator)
154
+ prevented = Dommy::Interaction::EventSynthesis.click(button)
155
+ if !prevented && submit_button?(button) && (form = finder.form_for(button))
156
+ form.dispatch_event(Dommy::Event.new("submit", "bubbles" => true, "cancelable" => true))
157
+ end
158
+ after_interaction
159
+ button
160
+ end
161
+
162
+ # Click a link, firing its click event so SPA JS (Turbo, React Router, …)
163
+ # can intercept. Real navigation on an un-prevented click is out of scope.
164
+ def click_link(locator)
165
+ link = finder.find_link(locator)
166
+ Dommy::Interaction::EventSynthesis.click(link)
167
+ after_interaction
168
+ link
169
+ end
170
+
171
+ # Suppress strict-mode failure for JS errors raised inside the block (they
172
+ # stay collected in #js_errors for inspection). For tests that expect errors.
173
+ def allow_js_errors
174
+ prev = @allow_errors
175
+ @allow_errors = true
176
+ yield
177
+ ensure
178
+ @allow_errors = prev
179
+ @acknowledged = @js_errors.length
180
+ end
181
+
182
+ def dispose
183
+ return if @disposed
184
+
185
+ @disposed = true
186
+ pending = unacknowledged
187
+ @runtime&.dispose
188
+ raise JsError, pending if @strict && !pending.empty?
189
+ end
190
+
191
+ private
192
+
193
+ def unacknowledged = @js_errors[@acknowledged..] || []
194
+
195
+ def submit_button?(button)
196
+ if button.tag_name == "BUTTON"
197
+ button.type == "submit"
198
+ else
199
+ %w[submit image].include?(button.type)
200
+ end
201
+ end
202
+
203
+ # In strict mode, fail on any JS error collected since the last
204
+ # acknowledgement. Marks all current errors acknowledged so each is reported
205
+ # at most once.
206
+ def check_js_errors!
207
+ return if @allow_errors
208
+ return unless @strict
209
+
210
+ pending = unacknowledged
211
+ return if pending.empty?
212
+
213
+ @acknowledged = @js_errors.length
214
+ raise JsError, pending
215
+ end
216
+ end
217
+ end
@@ -44,6 +44,8 @@ module Dommy
44
44
  @readable
45
45
  when "writable"
46
46
  @writable
47
+ else
48
+ Bridge::ABSENT
47
49
  end
48
50
  end
49
51
 
@@ -111,6 +113,8 @@ module Dommy
111
113
  @readable
112
114
  when "writable"
113
115
  @writable
116
+ else
117
+ Bridge::ABSENT
114
118
  end
115
119
  end
116
120
 
data/lib/dommy/crypto.rb CHANGED
@@ -59,6 +59,8 @@ module Dommy
59
59
  case key
60
60
  when "subtle"
61
61
  subtle
62
+ else
63
+ Bridge::ABSENT
62
64
  end
63
65
  end
64
66
 
@@ -396,6 +398,8 @@ module Dommy
396
398
  {"name" => @algorithm_name, "hash" => {"name" => @hash_name}}
397
399
  when "usages"
398
400
  @usages
401
+ else
402
+ Bridge::ABSENT
399
403
  end
400
404
  end
401
405
  end