ruby_wasm_ui 0.8.1

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/ruby_comments.mdc +29 -0
  3. data/.github/workflows/playwright.yml +74 -0
  4. data/.github/workflows/rspec.yml +33 -0
  5. data/.node-version +1 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +218 -0
  9. data/Rakefile +4 -0
  10. data/docs/conditional-rendering.md +119 -0
  11. data/docs/lifecycle-hooks.md +75 -0
  12. data/docs/list-rendering.md +51 -0
  13. data/examples/Gemfile +5 -0
  14. data/examples/Gemfile.lock +41 -0
  15. data/examples/Makefile +15 -0
  16. data/examples/npm-packages/runtime/counter/index.html +28 -0
  17. data/examples/npm-packages/runtime/counter/index.rb +62 -0
  18. data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
  19. data/examples/npm-packages/runtime/hello/index.html +28 -0
  20. data/examples/npm-packages/runtime/hello/index.rb +29 -0
  21. data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
  22. data/examples/npm-packages/runtime/input/index.html +28 -0
  23. data/examples/npm-packages/runtime/input/index.rb +46 -0
  24. data/examples/npm-packages/runtime/input/index.spec.js +58 -0
  25. data/examples/npm-packages/runtime/list/index.html +27 -0
  26. data/examples/npm-packages/runtime/list/index.rb +33 -0
  27. data/examples/npm-packages/runtime/list/index.spec.js +46 -0
  28. data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
  29. data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
  30. data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
  31. data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
  32. data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
  33. data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
  34. data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
  35. data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
  36. data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
  37. data/examples/npm-packages/runtime/search_field/index.html +27 -0
  38. data/examples/npm-packages/runtime/search_field/index.rb +39 -0
  39. data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
  40. data/examples/npm-packages/runtime/todos/index.html +28 -0
  41. data/examples/npm-packages/runtime/todos/index.rb +239 -0
  42. data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
  43. data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
  44. data/examples/package.json +12 -0
  45. data/examples/src/counter/index.html +23 -0
  46. data/examples/src/counter/index.rb +60 -0
  47. data/lib/ruby_wasm_ui +1 -0
  48. data/lib/ruby_wasm_ui.rb +1 -0
  49. data/package-lock.json +100 -0
  50. data/package.json +32 -0
  51. data/packages/npm-packages/runtime/Gemfile +3 -0
  52. data/packages/npm-packages/runtime/Gemfile.lock +26 -0
  53. data/packages/npm-packages/runtime/README.md +5 -0
  54. data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
  55. data/packages/npm-packages/runtime/package-lock.json +6668 -0
  56. data/packages/npm-packages/runtime/package.json +38 -0
  57. data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
  58. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
  59. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
  60. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
  61. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
  62. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
  63. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
  64. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
  65. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
  66. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
  67. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
  68. data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
  69. data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
  70. data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
  71. data/packages/npm-packages/runtime/src/index.js +37 -0
  72. data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
  73. data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
  74. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
  75. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
  76. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
  77. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
  78. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
  79. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
  80. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
  81. data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
  82. data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
  83. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
  84. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
  85. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
  86. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
  87. data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
  88. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
  89. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
  90. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
  91. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
  92. data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
  93. data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
  94. data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
  95. data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
  96. data/packages/npm-packages/runtime/vitest.config.js +8 -0
  97. data/playwright.config.js +78 -0
  98. data/sig/ruby_wasm_ui.rbs +4 -0
  99. metadata +168 -0
@@ -0,0 +1,573 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Template::BuildVdom do
6
+ let(:mock_elements) { double('elements') }
7
+ let(:mock_element_node) { double('element_node') }
8
+ let(:mock_template_node) { double('template_node') }
9
+ let(:mock_attributes) { double('attributes') }
10
+ let(:mock_node_constants) { double('Node') }
11
+
12
+ before do
13
+ # Mock JS.global[:Node] constants
14
+ js_mock = double('JS')
15
+ allow(js_mock).to receive(:global).and_return({ Node: mock_node_constants })
16
+ stub_const('JS', js_mock)
17
+ allow(mock_node_constants).to receive(:[]).with(:TEXT_NODE).and_return(3)
18
+ allow(mock_node_constants).to receive(:[]).with(:ELEMENT_NODE).and_return(1)
19
+ end
20
+
21
+ describe '.build' do
22
+ context 'when processing text nodes' do
23
+ let(:mock_text_node) { double('text_node') }
24
+
25
+ before do
26
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
27
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_text_node)
28
+ allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3)
29
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('Hello World')
30
+ end
31
+
32
+ it 'parses text nodes correctly' do
33
+ result = described_class.build(mock_elements)
34
+ expect(result).to eq('"Hello World"')
35
+ end
36
+ end
37
+
38
+ context 'when processing text nodes with embedded scripts' do
39
+ let(:mock_text_node) { double('text_node') }
40
+
41
+ before do
42
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
43
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_text_node)
44
+ allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3)
45
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('Count: {state[:count]}')
46
+ end
47
+
48
+ it 'converts embedded scripts to Ruby interpolation' do
49
+ result = described_class.build(mock_elements)
50
+ expect(result).to eq('"Count: #{state[:count]}"')
51
+ end
52
+ end
53
+
54
+ context 'when processing empty text nodes' do
55
+ let(:mock_text_node) { double('text_node') }
56
+
57
+ before do
58
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
59
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_text_node)
60
+ allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3)
61
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return(' ')
62
+ end
63
+
64
+ it 'ignores empty or whitespace-only text nodes' do
65
+ result = described_class.build(mock_elements)
66
+ expect(result).to eq('')
67
+ end
68
+ end
69
+
70
+ context 'when processing element nodes' do
71
+ let(:mock_child_elements) { double('child_elements') }
72
+
73
+ before do
74
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
75
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_element_node)
76
+ allow(mock_element_node).to receive(:[]).with(:nodeType).and_return(1)
77
+ allow(mock_element_node).to receive(:[]).with(:tagName).and_return('DIV')
78
+ allow(mock_element_node).to receive(:[]).with(:attributes).and_return(mock_attributes)
79
+ allow(mock_element_node).to receive(:[]).with(:childNodes).and_return(mock_child_elements)
80
+
81
+ # Mock attributes.to_a and :length for has_data_template_attribute?
82
+ allow(mock_attributes).to receive(:to_a).and_return([])
83
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(0)
84
+
85
+ # Mock empty children
86
+ allow(mock_child_elements).to receive(:[]).with(:length).and_return(0)
87
+
88
+ # Mock has_conditional_attribute? to return false
89
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_element_node).and_return(false)
90
+ # Mock has_for_attribute? to return false
91
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_element_node).and_return(false)
92
+ end
93
+
94
+ it 'builds VDOM for element nodes' do
95
+ result = described_class.build(mock_elements)
96
+ expect(result).to eq("RubyWasmUi::Vdom.h('div', {}, [])")
97
+ end
98
+ end
99
+
100
+ context 'when processing template (fragment) nodes' do
101
+ let(:mock_content) { double('content') }
102
+ let(:mock_child_nodes) { double('child_nodes') }
103
+
104
+ before do
105
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
106
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_template_node)
107
+ allow(mock_template_node).to receive(:[]).with(:nodeType).and_return(1)
108
+ allow(mock_template_node).to receive(:[]).with(:tagName).and_return('TEMPLATE')
109
+ allow(mock_template_node).to receive(:[]).with(:content).and_return(mock_content)
110
+ allow(mock_content).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
111
+
112
+ # Mock empty children
113
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
114
+
115
+ # Mock has_conditional_attribute? to return false
116
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_template_node).and_return(false)
117
+ end
118
+
119
+ it 'builds VDOM for fragment nodes' do
120
+ result = described_class.build(mock_elements)
121
+ expect(result).to eq("RubyWasmUi::Vdom.h_fragment([])")
122
+ end
123
+ end
124
+
125
+ context 'when processing div elements with data-template attribute' do
126
+ let(:mock_div_template_node) { double('div_template_node') }
127
+ let(:mock_child_nodes) { double('child_nodes') }
128
+ let(:mock_attributes) { double('attributes') }
129
+ let(:mock_attribute) { double('attribute') }
130
+
131
+ before do
132
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
133
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_div_template_node)
134
+ allow(mock_div_template_node).to receive(:[]).with(:nodeType).and_return(1)
135
+ allow(mock_div_template_node).to receive(:[]).with(:tagName).and_return('DIV')
136
+ allow(mock_div_template_node).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
137
+ allow(mock_div_template_node).to receive(:[]).with(:attributes).and_return(mock_attributes)
138
+
139
+ # Mock data-template attribute
140
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
141
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
142
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('data-template')
143
+
144
+ # Mock empty children
145
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
146
+
147
+ # Mock has_conditional_attribute? to return false
148
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_div_template_node).and_return(false)
149
+ end
150
+
151
+ it 'builds VDOM for div elements with data-template attribute as fragments' do
152
+ result = described_class.build(mock_elements)
153
+ expect(result).to eq("RubyWasmUi::Vdom.h_fragment([])")
154
+ end
155
+
156
+ it 'uses childNodes directly for div elements with data-template' do
157
+ described_class.build(mock_elements)
158
+ expect(mock_div_template_node).to have_received(:[]).with(:childNodes)
159
+ end
160
+ end
161
+
162
+ context 'when processing components' do
163
+ let(:mock_component_element) { double('component_element') }
164
+ let(:mock_component_attributes) { double('component_attributes') }
165
+ let(:mock_component_children) { double('component_children') }
166
+
167
+ before do
168
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
169
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_component_element)
170
+ allow(mock_component_element).to receive(:[]).with(:nodeType).and_return(1)
171
+ allow(mock_component_element).to receive(:[]).with(:tagName).and_return('CUSTOM-COMPONENT')
172
+ allow(mock_component_element).to receive(:[]).with(:attributes).and_return(mock_component_attributes)
173
+ allow(mock_component_element).to receive(:[]).with(:childNodes).and_return(mock_component_children)
174
+
175
+ # Mock attributes and children
176
+ allow(mock_component_attributes).to receive(:to_a).and_return([])
177
+ allow(mock_component_children).to receive(:[]).with(:length).and_return(0)
178
+
179
+ # Mock has_conditional_attribute? to return false
180
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_component_element).and_return(false)
181
+ # Mock has_for_attribute? to return false
182
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_component_element).and_return(false)
183
+ end
184
+
185
+ it 'builds VDOM for components with PascalCase names' do
186
+ result = described_class.build(mock_elements)
187
+ expect(result).to eq("RubyWasmUi::Vdom.h(CustomComponent, {}, [])")
188
+ end
189
+ end
190
+
191
+ context 'when processing components with conditional attributes' do
192
+ let(:mock_component_elements) { double('component_elements') }
193
+ let(:mock_component_element) { double('component_element') }
194
+ let(:mock_component_attributes) { double('component_attributes') }
195
+ let(:mock_conditional_attribute) { double('conditional_attribute') }
196
+
197
+ before do
198
+ allow(mock_component_elements).to receive(:[]).with(:length).and_return(1)
199
+ allow(mock_component_elements).to receive(:[]).with(0).and_return(mock_component_element)
200
+ allow(mock_component_element).to receive(:[]).with(:nodeType).and_return(1)
201
+ allow(mock_component_element).to receive(:[]).with(:tagName).and_return('CUSTOM-COMPONENT')
202
+ allow(mock_component_element).to receive(:[]).with(:attributes).and_return(mock_component_attributes)
203
+
204
+ # Mock has_conditional_attribute? to return true
205
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_component_element).and_return(true)
206
+ # Mock has_for_attribute? to return false
207
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_component_element).and_return(false)
208
+
209
+ # Mock conditional attribute
210
+ allow(mock_component_attributes).to receive(:[]).with(:length).and_return(1)
211
+ allow(mock_component_attributes).to receive(:[]).with(0).and_return(mock_conditional_attribute)
212
+ allow(mock_conditional_attribute).to receive(:[]).with(:name).and_return('r-if')
213
+ allow(mock_conditional_attribute).to receive(:[]).with(:value).and_return('condition')
214
+
215
+ # Mock build_conditional_group method
216
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:build_conditional_group)
217
+ .with(mock_component_elements, 0)
218
+ .and_return(['conditional_code', 1])
219
+ end
220
+
221
+ it 'processes conditional attributes on components' do
222
+ result = described_class.build(mock_component_elements)
223
+ expect(result).to eq("conditional_code")
224
+ expect(RubyWasmUi::Template::BuildConditionalGroup).to have_received(:build_conditional_group)
225
+ end
226
+ end
227
+
228
+ context 'when processing components with r-for attributes' do
229
+ let(:mock_component_elements) { double('component_elements') }
230
+ let(:mock_component_element) { double('component_element') }
231
+ let(:mock_component_attributes) { double('component_attributes') }
232
+ let(:mock_for_attribute) { double('for_attribute') }
233
+
234
+ before do
235
+ allow(mock_component_elements).to receive(:[]).with(:length).and_return(1)
236
+ allow(mock_component_elements).to receive(:[]).with(0).and_return(mock_component_element)
237
+ allow(mock_component_element).to receive(:[]).with(:nodeType).and_return(1)
238
+ allow(mock_component_element).to receive(:[]).with(:tagName).and_return('TODO-ITEM-COMPONENT')
239
+ allow(mock_component_element).to receive(:[]).with(:attributes).and_return(mock_component_attributes)
240
+
241
+ # Mock has_conditional_attribute? to return false
242
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_component_element).and_return(false)
243
+ # Mock has_for_attribute? to return true
244
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_component_element).and_return(true)
245
+
246
+ # Mock r-for attribute
247
+ allow(mock_component_attributes).to receive(:[]).with(:length).and_return(1)
248
+ allow(mock_component_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
249
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
250
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('{todo in todos}')
251
+
252
+ # Mock build_for_loop method
253
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:build_for_loop)
254
+ .with(mock_component_element)
255
+ .and_return("todos.map do |todo|\n RubyWasmUi::Vdom.h(TodoItemComponent, {}, [])\nend")
256
+ end
257
+
258
+ it 'processes r-for attributes on components with splat operator' do
259
+ result = described_class.build(mock_component_elements)
260
+ expected = "*todos.map do |todo|\n RubyWasmUi::Vdom.h(TodoItemComponent, {}, [])\nend"
261
+ expect(result).to eq(expected)
262
+ expect(RubyWasmUi::Template::BuildForGroup).to have_received(:build_for_loop)
263
+ end
264
+ end
265
+
266
+ context 'when processing elements with r-for attributes' do
267
+ let(:mock_element_array) { double('element_array') }
268
+ let(:mock_li_element) { double('li_element') }
269
+ let(:mock_element_attributes) { double('element_attributes') }
270
+ let(:mock_for_attribute) { double('for_attribute') }
271
+
272
+ before do
273
+ allow(mock_element_array).to receive(:[]).with(:length).and_return(1)
274
+ allow(mock_element_array).to receive(:[]).with(0).and_return(mock_li_element)
275
+ allow(mock_li_element).to receive(:[]).with(:nodeType).and_return(1)
276
+ allow(mock_li_element).to receive(:[]).with(:tagName).and_return('LI')
277
+ allow(mock_li_element).to receive(:[]).with(:attributes).and_return(mock_element_attributes)
278
+
279
+ # Mock has_conditional_attribute? to return false
280
+ allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_li_element).and_return(false)
281
+ # Mock has_for_attribute? to return true
282
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_li_element).and_return(true)
283
+
284
+ # Mock r-for attribute
285
+ allow(mock_element_attributes).to receive(:[]).with(:length).and_return(1)
286
+ allow(mock_element_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
287
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
288
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('{item in items}')
289
+
290
+ # Mock build_for_loop method
291
+ allow(RubyWasmUi::Template::BuildForGroup).to receive(:build_for_loop)
292
+ .with(mock_li_element)
293
+ .and_return("items.map do |item|\n RubyWasmUi::Vdom.h('li', {}, [])\nend")
294
+ end
295
+
296
+ it 'processes r-for attributes on HTML elements with splat operator' do
297
+ result = described_class.build(mock_element_array)
298
+ expected = "*items.map do |item|\n RubyWasmUi::Vdom.h('li', {}, [])\nend"
299
+ expect(result).to eq(expected)
300
+ expect(RubyWasmUi::Template::BuildForGroup).to have_received(:build_for_loop)
301
+ end
302
+ end
303
+ end
304
+
305
+ describe '.has_data_template_attribute?' do
306
+ let(:mock_element) { double('element') }
307
+ let(:mock_attributes) { double('attributes') }
308
+ let(:mock_attribute) { double('attribute') }
309
+
310
+ context 'when element has data-template attribute' do
311
+ before do
312
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
313
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
314
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
315
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('data-template')
316
+ end
317
+
318
+ it 'returns true' do
319
+ result = described_class.has_data_template_attribute?(mock_element)
320
+ expect(result).to be true
321
+ end
322
+ end
323
+
324
+ context 'when element has no data-template attribute' do
325
+ before do
326
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
327
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
328
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
329
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
330
+ end
331
+
332
+ it 'returns false' do
333
+ result = described_class.has_data_template_attribute?(mock_element)
334
+ expect(result).to be false
335
+ end
336
+ end
337
+
338
+ context 'when element has no attributes' do
339
+ before do
340
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(nil)
341
+ end
342
+
343
+ it 'returns false' do
344
+ result = described_class.has_data_template_attribute?(mock_element)
345
+ expect(result).to be false
346
+ end
347
+ end
348
+ end
349
+
350
+ describe '.parse_text_node' do
351
+ let(:mock_text_node) { double('text_node') }
352
+
353
+ context 'when text contains embedded script' do
354
+ before do
355
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('Hello {name}!')
356
+ end
357
+
358
+ it 'converts to Ruby string interpolation' do
359
+ result = described_class.parse_text_node(mock_text_node)
360
+ expect(result).to eq('"Hello #{name}!"')
361
+ end
362
+ end
363
+
364
+ context 'when text contains multiple embedded scripts' do
365
+ before do
366
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('{greeting} {name}!')
367
+ end
368
+
369
+ it 'converts all embedded scripts to Ruby interpolation' do
370
+ result = described_class.parse_text_node(mock_text_node)
371
+ expect(result).to eq('"#{greeting} #{name}!"')
372
+ end
373
+ end
374
+
375
+ context 'when text has no embedded script' do
376
+ before do
377
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('Plain text')
378
+ end
379
+
380
+ it 'returns quoted string' do
381
+ result = described_class.parse_text_node(mock_text_node)
382
+ expect(result).to eq('"Plain text"')
383
+ end
384
+ end
385
+
386
+ context 'when text is empty or whitespace only' do
387
+ before do
388
+ allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return(' ')
389
+ end
390
+
391
+ it 'returns nil' do
392
+ result = described_class.parse_text_node(mock_text_node)
393
+ expect(result).to be_nil
394
+ end
395
+ end
396
+ end
397
+
398
+ describe '.parse_attributes' do
399
+ let(:mock_attribute1) { double('attribute1') }
400
+ let(:mock_attribute2) { double('attribute2') }
401
+ let(:attributes) { [mock_attribute1, mock_attribute2] }
402
+
403
+ context 'when attributes contain embedded scripts' do
404
+ before do
405
+ allow(mock_attribute1).to receive(:[]).with(:name).and_return('class')
406
+ allow(mock_attribute1).to receive(:[]).with(:value).and_return('btn {btnClass}')
407
+ allow(mock_attribute2).to receive(:[]).with(:name).and_return('id')
408
+ allow(mock_attribute2).to receive(:[]).with(:value).and_return('button-1')
409
+ end
410
+
411
+ it 'processes embedded scripts correctly' do
412
+ result = described_class.parse_attributes(attributes)
413
+ expect(result).to eq(":class => btn btnClass, :id => 'button-1'")
414
+ end
415
+ end
416
+
417
+ context 'when attribute is "on" with hash value' do
418
+ let(:mock_on_attribute) { double('on_attribute') }
419
+ let(:attributes) { [mock_on_attribute] }
420
+
421
+ before do
422
+ allow(mock_on_attribute).to receive(:[]).with(:name).and_return('on')
423
+ allow(mock_on_attribute).to receive(:[]).with(:value).and_return('{ click: handleClick }')
424
+ end
425
+
426
+ it 'preserves hash structure for "on" attribute' do
427
+ result = described_class.parse_attributes(attributes)
428
+ expect(result).to eq(":on => { click: handleClick }")
429
+ end
430
+ end
431
+
432
+ context 'when attributes have no embedded scripts' do
433
+ before do
434
+ allow(mock_attribute1).to receive(:[]).with(:name).and_return('class')
435
+ allow(mock_attribute1).to receive(:[]).with(:value).and_return('btn')
436
+ allow(mock_attribute2).to receive(:[]).with(:name).and_return('id')
437
+ allow(mock_attribute2).to receive(:[]).with(:value).and_return('button-1')
438
+ end
439
+
440
+ it 'returns quoted string values' do
441
+ result = described_class.parse_attributes(attributes)
442
+ expect(result).to eq(":class => 'btn', :id => 'button-1'")
443
+ end
444
+ end
445
+ end
446
+
447
+ describe '.is_component?' do
448
+ it 'returns true for kebab-case component names' do
449
+ expect(described_class.is_component?('my-component')).to be true
450
+ end
451
+
452
+ it 'returns true for single word component names' do
453
+ expect(described_class.is_component?('mycomponent')).to be true
454
+ end
455
+
456
+ it 'returns false for standard HTML elements' do
457
+ expect(described_class.is_component?('div')).to be false
458
+ expect(described_class.is_component?('span')).to be false
459
+ expect(described_class.is_component?('button')).to be false
460
+ end
461
+
462
+ it 'returns false for template element' do
463
+ expect(described_class.is_component?('template')).to be false
464
+ end
465
+ end
466
+
467
+ describe '.embed_script?' do
468
+ it 'returns true for strings with curly braces' do
469
+ expect(described_class.embed_script?('{variable}')).to be true
470
+ expect(described_class.embed_script?('text {variable} more')).to be true
471
+ end
472
+
473
+ it 'returns false for strings without curly braces' do
474
+ expect(described_class.embed_script?('plain text')).to be false
475
+ expect(described_class.embed_script?('no braces here')).to be false
476
+ end
477
+ end
478
+
479
+ describe '.get_embed_script' do
480
+ it 'extracts content from curly braces' do
481
+ expect(described_class.get_embed_script('{variable}')).to eq('variable')
482
+ expect(described_class.get_embed_script('{state[:count]}')).to eq('state[:count]')
483
+ end
484
+
485
+ it 'extracts first occurrence when multiple braces' do
486
+ expect(described_class.get_embed_script('{first} {second}')).to eq('first} {second')
487
+ end
488
+ end
489
+
490
+ describe '.build_fragment' do
491
+ let(:mock_element) { double('element') }
492
+ let(:mock_content) { double('content') }
493
+ let(:mock_child_nodes) { double('child_nodes') }
494
+
495
+ context 'when element is a template with content property' do
496
+ before do
497
+ allow(mock_element).to receive(:[]).with(:content).and_return(mock_content)
498
+ allow(mock_content).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
499
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
500
+ end
501
+
502
+ it 'builds fragment using content childNodes' do
503
+ result = described_class.build_fragment(mock_element, 'template')
504
+ expect(result).to eq("RubyWasmUi::Vdom.h_fragment([])")
505
+ end
506
+ end
507
+
508
+ context 'when element is a div with data-template' do
509
+ before do
510
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
511
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
512
+ end
513
+
514
+ it 'builds fragment using direct childNodes' do
515
+ result = described_class.build_fragment(mock_element, 'div')
516
+ expect(result).to eq("RubyWasmUi::Vdom.h_fragment([])")
517
+ end
518
+ end
519
+ end
520
+
521
+ describe '.build_component' do
522
+ let(:mock_element) { double('element') }
523
+ let(:mock_attributes) { double('attributes') }
524
+ let(:mock_child_nodes) { double('child_nodes') }
525
+
526
+ before do
527
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
528
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
529
+ allow(mock_attributes).to receive(:to_a).and_return([])
530
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
531
+ allow(described_class).to receive(:parse_attributes).with([]).and_return('')
532
+ end
533
+
534
+ it 'builds component with PascalCase name conversion' do
535
+ result = described_class.build_component(mock_element, 'custom-component')
536
+ expect(result).to eq("RubyWasmUi::Vdom.h(CustomComponent, {}, [])")
537
+ end
538
+
539
+ it 'builds multi-word component with PascalCase name conversion' do
540
+ result = described_class.build_component(mock_element, 'my-custom-button')
541
+ expect(result).to eq("RubyWasmUi::Vdom.h(MyCustomButton, {}, [])")
542
+ end
543
+
544
+ it 'builds single word component' do
545
+ result = described_class.build_component(mock_element, 'mycomponent')
546
+ expect(result).to eq("RubyWasmUi::Vdom.h(Mycomponent, {}, [])")
547
+ end
548
+ end
549
+
550
+ describe '.build_element' do
551
+ let(:mock_element) { double('element') }
552
+ let(:mock_attributes) { double('attributes') }
553
+ let(:mock_child_nodes) { double('child_nodes') }
554
+
555
+ before do
556
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
557
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
558
+ allow(mock_attributes).to receive(:to_a).and_return([])
559
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
560
+ allow(described_class).to receive(:parse_attributes).with([]).and_return('')
561
+ end
562
+
563
+ it 'builds regular HTML element' do
564
+ result = described_class.build_element(mock_element, 'div')
565
+ expect(result).to eq("RubyWasmUi::Vdom.h('div', {}, [])")
566
+ end
567
+
568
+ it 'builds HTML element with lowercase tag name' do
569
+ result = described_class.build_element(mock_element, 'span')
570
+ expect(result).to eq("RubyWasmUi::Vdom.h('span', {}, [])")
571
+ end
572
+ end
573
+ end