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.
- checksums.yaml +7 -0
- data/.cursor/rules/ruby_comments.mdc +29 -0
- data/.github/workflows/playwright.yml +74 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +4 -0
- data/docs/conditional-rendering.md +119 -0
- data/docs/lifecycle-hooks.md +75 -0
- data/docs/list-rendering.md +51 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +41 -0
- data/examples/Makefile +15 -0
- data/examples/npm-packages/runtime/counter/index.html +28 -0
- data/examples/npm-packages/runtime/counter/index.rb +62 -0
- data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
- data/examples/npm-packages/runtime/hello/index.html +28 -0
- data/examples/npm-packages/runtime/hello/index.rb +29 -0
- data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
- data/examples/npm-packages/runtime/input/index.html +28 -0
- data/examples/npm-packages/runtime/input/index.rb +46 -0
- data/examples/npm-packages/runtime/input/index.spec.js +58 -0
- data/examples/npm-packages/runtime/list/index.html +27 -0
- data/examples/npm-packages/runtime/list/index.rb +33 -0
- data/examples/npm-packages/runtime/list/index.spec.js +46 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
- data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
- data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
- data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
- data/examples/npm-packages/runtime/search_field/index.html +27 -0
- data/examples/npm-packages/runtime/search_field/index.rb +39 -0
- data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
- data/examples/npm-packages/runtime/todos/index.html +28 -0
- data/examples/npm-packages/runtime/todos/index.rb +239 -0
- data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
- data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
- data/examples/package.json +12 -0
- data/examples/src/counter/index.html +23 -0
- data/examples/src/counter/index.rb +60 -0
- data/lib/ruby_wasm_ui +1 -0
- data/lib/ruby_wasm_ui.rb +1 -0
- data/package-lock.json +100 -0
- data/package.json +32 -0
- data/packages/npm-packages/runtime/Gemfile +3 -0
- data/packages/npm-packages/runtime/Gemfile.lock +26 -0
- data/packages/npm-packages/runtime/README.md +5 -0
- data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
- data/packages/npm-packages/runtime/package-lock.json +6668 -0
- data/packages/npm-packages/runtime/package.json +38 -0
- data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
- data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
- data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
- data/packages/npm-packages/runtime/src/index.js +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruby_wasm_ui.rbs +4 -0
- 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
|