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,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Template::BuildForGroup do
6
+ let(:mock_node_constants) { double('Node') }
7
+
8
+ before do
9
+ # Mock JS.global[:Node] constants
10
+ js_mock = double('JS')
11
+ allow(js_mock).to receive(:global).and_return({ Node: mock_node_constants })
12
+ stub_const('JS', js_mock)
13
+
14
+ allow(mock_node_constants).to receive(:[]).with(:ELEMENT_NODE).and_return(1)
15
+ allow(mock_node_constants).to receive(:[]).with(:TEXT_NODE).and_return(3)
16
+ end
17
+
18
+ describe '.has_for_attribute?' do
19
+ let(:mock_element) { double('element') }
20
+ let(:mock_attributes) { double('attributes') }
21
+
22
+ context 'when element has r-for attribute' do
23
+ before do
24
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
25
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
26
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
27
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-for')
28
+ end
29
+
30
+ let(:mock_attribute) { double('attribute') }
31
+
32
+ it 'returns true' do
33
+ result = described_class.has_for_attribute?(mock_element)
34
+ expect(result).to be true
35
+ end
36
+ end
37
+
38
+ context 'when element has no r-for attribute' do
39
+ before do
40
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
41
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
42
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
43
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
44
+ end
45
+
46
+ let(:mock_attribute) { double('attribute') }
47
+
48
+ it 'returns false' do
49
+ result = described_class.has_for_attribute?(mock_element)
50
+ expect(result).to be false
51
+ end
52
+ end
53
+
54
+ context 'when element has no attributes' do
55
+ before do
56
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(nil)
57
+ end
58
+
59
+ it 'returns false' do
60
+ result = described_class.has_for_attribute?(mock_element)
61
+ expect(result).to be false
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '.build_for_loop' do
67
+ let(:mock_element) { double('element') }
68
+ let(:mock_attributes) { double('attributes') }
69
+ let(:mock_child_nodes) { double('child_nodes') }
70
+
71
+ before do
72
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('TODO-ITEM-COMPONENT')
73
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
74
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
75
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
76
+ end
77
+
78
+ context 'when element has valid r-for expression' do
79
+ let(:mock_for_attribute) { double('for_attribute') }
80
+ let(:mock_key_attribute) { double('key_attribute') }
81
+ let(:mock_todo_attribute) { double('todo_attribute') }
82
+ let(:mock_id_attribute) { double('id_attribute') }
83
+
84
+ before do
85
+ # Mock attributes
86
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(4)
87
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
88
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_key_attribute)
89
+ allow(mock_attributes).to receive(:[]).with(2).and_return(mock_todo_attribute)
90
+ allow(mock_attributes).to receive(:[]).with(3).and_return(mock_id_attribute)
91
+
92
+ # Mock r-for attribute
93
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
94
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('{todo in todos}')
95
+
96
+ # Mock other attributes
97
+ allow(mock_key_attribute).to receive(:[]).with(:name).and_return('key')
98
+ allow(mock_key_attribute).to receive(:[]).with(:value).and_return('{todo[:text]}')
99
+ allow(mock_todo_attribute).to receive(:[]).with(:name).and_return('todo')
100
+ allow(mock_todo_attribute).to receive(:[]).with(:value).and_return('{todo[:text]}')
101
+ allow(mock_id_attribute).to receive(:[]).with(:name).and_return('id')
102
+ allow(mock_id_attribute).to receive(:[]).with(:value).and_return('{todo[:id]}')
103
+
104
+ # Mock BuildVdom methods
105
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('{todo in todos}').and_return(true)
106
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:get_embed_script).with('{todo in todos}').and_return('todo in todos')
107
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:is_component?).with('todo-item-component').and_return(true)
108
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:parse_attributes).and_return(':key => todo[:text], :todo => todo[:text], :id => todo[:id]')
109
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build).with(mock_child_nodes).and_return('')
110
+ end
111
+
112
+ it 'generates map code for component' do
113
+ result = described_class.build_for_loop(mock_element)
114
+ expected = "todos.map do |todo|\n RubyWasmUi::Vdom.h(TodoItemComponent, {:key => todo[:text], :todo => todo[:text], :id => todo[:id]}, [])\nend"
115
+ expect(result).to eq(expected)
116
+ end
117
+ end
118
+
119
+ context 'when element has r-for expression without curly braces' do
120
+ let(:mock_for_attribute) { double('for_attribute') }
121
+
122
+ before do
123
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
124
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
125
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
126
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('item in items')
127
+
128
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('item in items').and_return(false)
129
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:is_component?).with('todo-item-component').and_return(true)
130
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:parse_attributes).and_return('')
131
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build).with(mock_child_nodes).and_return('')
132
+ end
133
+
134
+ it 'generates map code without script extraction' do
135
+ result = described_class.build_for_loop(mock_element)
136
+ expected = "items.map do |item|\n RubyWasmUi::Vdom.h(TodoItemComponent, {}, [])\nend"
137
+ expect(result).to eq(expected)
138
+ end
139
+ end
140
+
141
+ context 'when element is a regular HTML element' do
142
+ before do
143
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('LI')
144
+
145
+ # Mock r-for attribute
146
+ mock_for_attribute = double('for_attribute')
147
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
148
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
149
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
150
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('{item in items}')
151
+
152
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('{item in items}').and_return(true)
153
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:get_embed_script).with('{item in items}').and_return('item in items')
154
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:is_component?).with('li').and_return(false)
155
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:parse_attributes).and_return('')
156
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build).with(mock_child_nodes).and_return('')
157
+ end
158
+
159
+ it 'generates map code for HTML element' do
160
+ result = described_class.build_for_loop(mock_element)
161
+ expected = "items.map do |item|\n RubyWasmUi::Vdom.h('li', {}, [])\nend"
162
+ expect(result).to eq(expected)
163
+ end
164
+ end
165
+
166
+ context 'when r-for expression has invalid syntax' do
167
+ let(:mock_for_attribute) { double('for_attribute') }
168
+
169
+ before do
170
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
171
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
172
+ allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
173
+ allow(mock_for_attribute).to receive(:[]).with(:value).and_return('invalid syntax')
174
+
175
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('invalid syntax').and_return(false)
176
+ end
177
+
178
+ it 'returns empty string' do
179
+ result = described_class.build_for_loop(mock_element)
180
+ expect(result).to eq('')
181
+ end
182
+ end
183
+
184
+ context 'when element has no r-for attribute' do
185
+ before do
186
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(0)
187
+ end
188
+
189
+ it 'returns empty string' do
190
+ result = described_class.build_for_loop(mock_element)
191
+ expect(result).to eq('')
192
+ end
193
+ end
194
+ end
195
+
196
+ describe '.get_for_expression' do
197
+ let(:mock_element) { double('element') }
198
+ let(:mock_attributes) { double('attributes') }
199
+ let(:mock_attribute) { double('attribute') }
200
+
201
+ before do
202
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
203
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
204
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
205
+ end
206
+
207
+ context 'when element has r-for attribute with embedded script' do
208
+ before do
209
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-for')
210
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('{item in items}')
211
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('{item in items}').and_return(true)
212
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:get_embed_script).with('{item in items}').and_return('item in items')
213
+ end
214
+
215
+ it 'returns the extracted script' do
216
+ result = described_class.get_for_expression(mock_element)
217
+ expect(result).to eq('item in items')
218
+ end
219
+ end
220
+
221
+ context 'when element has r-for attribute with plain value' do
222
+ before do
223
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-for')
224
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('item in items')
225
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('item in items').and_return(false)
226
+ end
227
+
228
+ it 'returns the plain value' do
229
+ result = described_class.get_for_expression(mock_element)
230
+ expect(result).to eq('item in items')
231
+ end
232
+ end
233
+
234
+ context 'when element has no r-for attribute' do
235
+ before do
236
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
237
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('some-class')
238
+ end
239
+
240
+ it 'returns nil' do
241
+ result = described_class.get_for_expression(mock_element)
242
+ expect(result).to be_nil
243
+ end
244
+ end
245
+ end
246
+
247
+ describe '.filter_for_attributes' do
248
+ let(:mock_attributes) { double('attributes') }
249
+ let(:mock_attribute_1) { double('attribute_1') }
250
+ let(:mock_attribute_2) { double('attribute_2') }
251
+ let(:mock_attribute_3) { double('attribute_3') }
252
+
253
+ context 'when attributes contain r-for and other attributes' do
254
+ before do
255
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(3)
256
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
257
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_attribute_2)
258
+ allow(mock_attributes).to receive(:[]).with(2).and_return(mock_attribute_3)
259
+
260
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('r-for')
261
+ allow(mock_attribute_1).to receive(:[]).with(:value).and_return('{item in items}')
262
+ allow(mock_attribute_2).to receive(:[]).with(:name).and_return('class')
263
+ allow(mock_attribute_2).to receive(:[]).with(:value).and_return('item-class')
264
+ allow(mock_attribute_3).to receive(:[]).with(:name).and_return('id')
265
+ allow(mock_attribute_3).to receive(:[]).with(:value).and_return('{item.id}')
266
+ end
267
+
268
+ it 'filters out r-for attribute and returns others in correct format' do
269
+ result = described_class.filter_for_attributes(mock_attributes)
270
+ expected = [
271
+ { name: 'class', value: 'item-class' },
272
+ { name: 'id', value: '{item.id}' }
273
+ ]
274
+ expect(result).to eq(expected)
275
+ end
276
+ end
277
+
278
+ context 'when attributes contain only r-for attribute' do
279
+ before do
280
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
281
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
282
+
283
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('r-for')
284
+ allow(mock_attribute_1).to receive(:[]).with(:value).and_return('{item in items}')
285
+ end
286
+
287
+ it 'returns empty array' do
288
+ result = described_class.filter_for_attributes(mock_attributes)
289
+ expect(result).to eq([])
290
+ end
291
+ end
292
+
293
+ context 'when attributes contain no r-for attribute' do
294
+ before do
295
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(2)
296
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
297
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_attribute_2)
298
+
299
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('class')
300
+ allow(mock_attribute_1).to receive(:[]).with(:value).and_return('item-class')
301
+ allow(mock_attribute_2).to receive(:[]).with(:name).and_return('id')
302
+ allow(mock_attribute_2).to receive(:[]).with(:value).and_return('item-id')
303
+ end
304
+
305
+ it 'returns all attributes in correct format' do
306
+ result = described_class.filter_for_attributes(mock_attributes)
307
+ expected = [
308
+ { name: 'class', value: 'item-class' },
309
+ { name: 'id', value: 'item-id' }
310
+ ]
311
+ expect(result).to eq(expected)
312
+ end
313
+ end
314
+ end
315
+
316
+ describe '.build_component_for_item' do
317
+ let(:mock_element) { double('element') }
318
+ let(:mock_child_nodes) { double('child_nodes') }
319
+ let(:filtered_attributes) do
320
+ [
321
+ { name: 'key', value: '{todo[:text]}' },
322
+ { name: 'todo', value: '{todo[:text]}' },
323
+ { name: 'id', value: '{todo[:id]}' }
324
+ ]
325
+ end
326
+
327
+ before do
328
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
329
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
330
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:parse_attributes).with(filtered_attributes).and_return(':key => todo[:text], :todo => todo[:text], :id => todo[:id]')
331
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build).with(mock_child_nodes).and_return('')
332
+ end
333
+
334
+ it 'builds component with PascalCase name conversion' do
335
+ result = described_class.build_component_for_item(mock_element, 'todo-item-component', filtered_attributes, 'todo')
336
+ expect(result).to eq("RubyWasmUi::Vdom.h(TodoItemComponent, {:key => todo[:text], :todo => todo[:text], :id => todo[:id]}, [])")
337
+ end
338
+
339
+ it 'builds multi-word component with PascalCase name conversion' do
340
+ result = described_class.build_component_for_item(mock_element, 'my-custom-button', filtered_attributes, 'item')
341
+ expect(result).to eq("RubyWasmUi::Vdom.h(MyCustomButton, {:key => todo[:text], :todo => todo[:text], :id => todo[:id]}, [])")
342
+ end
343
+
344
+ it 'builds single word component' do
345
+ result = described_class.build_component_for_item(mock_element, 'mycomponent', filtered_attributes, 'item')
346
+ expect(result).to eq("RubyWasmUi::Vdom.h(Mycomponent, {:key => todo[:text], :todo => todo[:text], :id => todo[:id]}, [])")
347
+ end
348
+ end
349
+
350
+ describe '.build_element_for_item' do
351
+ let(:mock_element) { double('element') }
352
+ let(:mock_child_nodes) { double('child_nodes') }
353
+ let(:filtered_attributes) do
354
+ [
355
+ { name: 'class', value: 'item-class' },
356
+ { name: 'id', value: '{item.id}' }
357
+ ]
358
+ end
359
+
360
+ before do
361
+ allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
362
+ allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
363
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:parse_attributes).with(filtered_attributes).and_return(':class => \'item-class\', :id => item.id')
364
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build).with(mock_child_nodes).and_return('')
365
+ end
366
+
367
+ it 'builds regular HTML element' do
368
+ result = described_class.build_element_for_item(mock_element, 'li', filtered_attributes, 'item')
369
+ expect(result).to eq("RubyWasmUi::Vdom.h('li', {:class => 'item-class', :id => item.id}, [])")
370
+ end
371
+
372
+ it 'builds HTML element with lowercase tag name' do
373
+ result = described_class.build_element_for_item(mock_element, 'div', filtered_attributes, 'item')
374
+ expect(result).to eq("RubyWasmUi::Vdom.h('div', {:class => 'item-class', :id => item.id}, [])")
375
+ end
376
+ end
377
+ end