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,627 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyWasmUi::Template::Parser do
|
|
6
|
+
describe '.preprocess_pascal_case_component_name' do
|
|
7
|
+
context 'when processing PascalCase component names' do
|
|
8
|
+
it 'converts simple PascalCase component name' do
|
|
9
|
+
input = '<ButtonComponent>Click me</ButtonComponent>'
|
|
10
|
+
expected = '<button-component>Click me</button-component>'
|
|
11
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
12
|
+
expect(result).to eq(expected)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'converts component with attributes' do
|
|
16
|
+
input = '<ButtonComponent class="primary" disabled>Click me</ButtonComponent>'
|
|
17
|
+
expected = '<button-component class="primary" disabled>Click me</button-component>'
|
|
18
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
19
|
+
expect(result).to eq(expected)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'converts self-closing component' do
|
|
23
|
+
input = '<SearchField placeholder="Search..." />'
|
|
24
|
+
expected = '<search-field placeholder="Search..." />'
|
|
25
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
26
|
+
expect(result).to eq(expected)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'converts multiple components in the same template' do
|
|
30
|
+
input = '<ButtonComponent><SearchField /><IconComponent name="search" /></ButtonComponent>'
|
|
31
|
+
expected = '<button-component><search-field /><icon-component name="search" /></button-component>'
|
|
32
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
33
|
+
expect(result).to eq(expected)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'converts components with complex PascalCase names' do
|
|
37
|
+
input = '<TodoListItemComponent><UserProfileCardComponent /></TodoListItemComponent>'
|
|
38
|
+
expected = '<todo-list-item-component><user-profile-card-component /></todo-list-item-component>'
|
|
39
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
40
|
+
expect(result).to eq(expected)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'handles components with embedded Ruby expressions' do
|
|
44
|
+
input = '<ButtonComponent on="{ click: ->(e) { handle_click(e) } }">Count: {count}</ButtonComponent>'
|
|
45
|
+
expected = '<button-component on="{ click: ->(e) { handle_click(e) } }">Count: {count}</button-component>'
|
|
46
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
47
|
+
expect(result).to eq(expected)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when processing mixed content' do
|
|
52
|
+
it 'does not convert regular HTML elements' do
|
|
53
|
+
input = '<div><span>Text</span><ButtonComponent>Click me</ButtonComponent></div>'
|
|
54
|
+
expected = '<div><span>Text</span><button-component>Click me</button-component></div>'
|
|
55
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
56
|
+
expect(result).to eq(expected)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'preserves existing kebab-case components' do
|
|
60
|
+
input = '<custom-button><ButtonComponent>Click me</ButtonComponent></custom-button>'
|
|
61
|
+
expected = '<custom-button><button-component>Click me</button-component></custom-button>'
|
|
62
|
+
result = described_class.preprocess_pascal_case_component_name(input)
|
|
63
|
+
expect(result).to eq(expected)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '.preprocess_template_tag' do
|
|
69
|
+
context 'when processing template tags' do
|
|
70
|
+
it 'converts simple template tag' do
|
|
71
|
+
input = '<template>Hello World</template>'
|
|
72
|
+
expected = '<div data-template>Hello World</div>'
|
|
73
|
+
result = described_class.preprocess_template_tag(input)
|
|
74
|
+
expect(result).to eq(expected)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'converts template tag with attributes' do
|
|
78
|
+
input = '<template class="container" data-test="value">Content</template>'
|
|
79
|
+
expected = '<div data-template class="container" data-test="value">Content</div>'
|
|
80
|
+
result = described_class.preprocess_template_tag(input)
|
|
81
|
+
expect(result).to eq(expected)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'converts nested template tags' do
|
|
85
|
+
input = '<template><template>Nested</template></template>'
|
|
86
|
+
expected = '<div data-template><div data-template>Nested</div></div>'
|
|
87
|
+
result = described_class.preprocess_template_tag(input)
|
|
88
|
+
expect(result).to eq(expected)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'converts template tags with complex content' do
|
|
92
|
+
input = '<template><div>Text</div><ButtonComponent>Click</ButtonComponent></template>'
|
|
93
|
+
expected = '<div data-template><div>Text</div><ButtonComponent>Click</ButtonComponent></div>'
|
|
94
|
+
result = described_class.preprocess_template_tag(input)
|
|
95
|
+
expect(result).to eq(expected)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'preserves whitespace and indentation' do
|
|
99
|
+
input = " <template>\n <div>Content</div>\n </template>"
|
|
100
|
+
expected = " <div data-template>\n <div>Content</div>\n </div>"
|
|
101
|
+
result = described_class.preprocess_template_tag(input)
|
|
102
|
+
expect(result).to eq(expected)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '.preprocess_self_closing_tags' do
|
|
108
|
+
context 'when processing custom elements with self-closing tags' do
|
|
109
|
+
it 'converts simple custom element self-closing tag' do
|
|
110
|
+
input = '<search-field value="test" />'
|
|
111
|
+
expected = '<search-field value="test" ></search-field>'
|
|
112
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
113
|
+
expect(result).to eq(expected)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'converts custom element with multiple attributes' do
|
|
117
|
+
input = '<my-component id="1" class="test" data-value="123" />'
|
|
118
|
+
expected = '<my-component id="1" class="test" data-value="123" ></my-component>'
|
|
119
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
120
|
+
expect(result).to eq(expected)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'converts multiple custom elements' do
|
|
124
|
+
input = '<first-component /><second-component attr="value" />'
|
|
125
|
+
expected = '<first-component ></first-component><second-component attr="value" ></second-component>'
|
|
126
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
127
|
+
expect(result).to eq(expected)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'handles custom elements with complex attributes including lambdas' do
|
|
131
|
+
input = '<search-field value="{component.state[:search_term]}" on="{ search: ->(term) { update(term) } }" />'
|
|
132
|
+
expected = '<search-field value="{component.state[:search_term]}" on="{ search: ->(term) { update(term) } }" ></search-field>'
|
|
133
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
134
|
+
expect(result).to eq(expected)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'handles nested quotes in attributes' do
|
|
138
|
+
input = '<my-component data-json=\'{"key": "value"}\' />'
|
|
139
|
+
expected = '<my-component data-json=\'{"key": "value"}\' ></my-component>'
|
|
140
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
141
|
+
expect(result).to eq(expected)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'when processing PascalCase components with self-closing tags' do
|
|
146
|
+
it 'does not convert PascalCase component self-closing tag (handled by pascal case preprocessor)' do
|
|
147
|
+
input = '<TodoItemEditComponent edited="test" />'
|
|
148
|
+
expected = '<TodoItemEditComponent edited="test" />'
|
|
149
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
150
|
+
expect(result).to eq(expected)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'does not convert PascalCase component with multiple attributes (handled by pascal case preprocessor)' do
|
|
154
|
+
input = '<SearchFieldComponent id="1" placeholder="Search..." value="test" />'
|
|
155
|
+
expected = '<SearchFieldComponent id="1" placeholder="Search..." value="test" />'
|
|
156
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
157
|
+
expect(result).to eq(expected)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'does not convert multiple PascalCase components (handled by pascal case preprocessor)' do
|
|
161
|
+
input = '<ButtonComponent /><InputComponent type="text" />'
|
|
162
|
+
expected = '<ButtonComponent /><InputComponent type="text" />'
|
|
163
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
164
|
+
expect(result).to eq(expected)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'does not handle PascalCase components with complex attributes (handled by pascal case preprocessor)' do
|
|
168
|
+
input = '<TodoItemEditComponent edited="{component.state[:edited]}" on="{ save: ->() { component.save_edition } }" />'
|
|
169
|
+
expected = '<TodoItemEditComponent edited="{component.state[:edited]}" on="{ save: ->() { component.save_edition } }" />'
|
|
170
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
171
|
+
expect(result).to eq(expected)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'does not handle PascalCase components with r-if attributes (handled by pascal case preprocessor)' do
|
|
175
|
+
input = '<TodoItemEditComponent r-if="{component.state[:is_editing]}" edited="{component.state[:edited]}" />'
|
|
176
|
+
expected = '<TodoItemEditComponent r-if="{component.state[:is_editing]}" edited="{component.state[:edited]}" />'
|
|
177
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
178
|
+
expect(result).to eq(expected)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
context 'when processing standard HTML elements' do
|
|
183
|
+
it 'does not convert standard HTML void elements' do
|
|
184
|
+
input = '<input type="text" />'
|
|
185
|
+
expected = '<input type="text" />'
|
|
186
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
187
|
+
expect(result).to eq(expected)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'does not convert single word tags' do
|
|
191
|
+
input = '<div /><span />'
|
|
192
|
+
expected = '<div /><span />'
|
|
193
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
194
|
+
expect(result).to eq(expected)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'does not convert already closed custom elements' do
|
|
198
|
+
input = '<my-component></my-component>'
|
|
199
|
+
expected = '<my-component></my-component>'
|
|
200
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
201
|
+
expect(result).to eq(expected)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context 'when processing mixed content' do
|
|
206
|
+
it 'converts only custom elements in mixed HTML' do
|
|
207
|
+
input = '<div><input type="text" /><my-component /></div>'
|
|
208
|
+
expected = '<div><input type="text" /><my-component ></my-component></div>'
|
|
209
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
210
|
+
expect(result).to eq(expected)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'handles multiple custom elements with different naming patterns' do
|
|
214
|
+
input = '<search-field /><todo-list-item /><app-header-nav />'
|
|
215
|
+
expected = '<search-field ></search-field><todo-list-item ></todo-list-item><app-header-nav ></app-header-nav>'
|
|
216
|
+
result = described_class.preprocess_self_closing_tags(input)
|
|
217
|
+
expect(result).to eq(expected)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe '.parse' do
|
|
223
|
+
let(:mock_parser) { double('parser') }
|
|
224
|
+
let(:mock_document) { double('document') }
|
|
225
|
+
let(:mock_body) { double('body') }
|
|
226
|
+
let(:mock_child_nodes) { double('child_nodes') }
|
|
227
|
+
|
|
228
|
+
before do
|
|
229
|
+
# Mock preprocessing chain in the correct order
|
|
230
|
+
allow(described_class).to receive(:preprocess_template_tag).with('template_string').and_return('after_template_processing')
|
|
231
|
+
allow(described_class).to receive(:preprocess_pascal_case_component_name).with('after_template_processing').and_return('after_pascal_processing')
|
|
232
|
+
allow(described_class).to receive(:preprocess_self_closing_tags).with('after_pascal_processing').and_return('processed_template')
|
|
233
|
+
|
|
234
|
+
# Mock JS.eval and DOMParser
|
|
235
|
+
js_mock = double('JS')
|
|
236
|
+
allow(js_mock).to receive(:eval).with('return new DOMParser()').and_return(mock_parser)
|
|
237
|
+
allow(js_mock).to receive(:try_convert).with('processed_template').and_return('processed_template')
|
|
238
|
+
stub_const('JS', js_mock)
|
|
239
|
+
|
|
240
|
+
allow(mock_parser).to receive(:call).with(:parseFromString, 'processed_template', 'text/html').and_return(mock_document)
|
|
241
|
+
allow(mock_document).to receive(:getElementsByTagName).with('body').and_return([mock_body])
|
|
242
|
+
allow(mock_body).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
|
|
243
|
+
|
|
244
|
+
# Mock empty child nodes
|
|
245
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'preprocesses template, pascal case, then self-closing tags in correct order' do
|
|
249
|
+
expect(described_class).to receive(:preprocess_template_tag).with('template_string').and_return('after_template_processing')
|
|
250
|
+
expect(described_class).to receive(:preprocess_pascal_case_component_name).with('after_template_processing').and_return('after_pascal_processing')
|
|
251
|
+
expect(described_class).to receive(:preprocess_self_closing_tags).with('after_pascal_processing').and_return('processed_template')
|
|
252
|
+
described_class.parse('template_string')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'parses HTML template and returns VDOM string' do
|
|
256
|
+
result = described_class.parse('template_string')
|
|
257
|
+
expect(result).to eq('')
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
context 'when processing template tags' do
|
|
261
|
+
it 'replaces <template> with <div data-template>' do
|
|
262
|
+
template = '<template><div>content</div></template>'
|
|
263
|
+
expected_replacement = '<div data-template><div>content</div></div>'
|
|
264
|
+
|
|
265
|
+
# Mock the preprocessing chain
|
|
266
|
+
allow(described_class).to receive(:preprocess_template_tag).with(template).and_return(expected_replacement)
|
|
267
|
+
allow(described_class).to receive(:preprocess_pascal_case_component_name).with(expected_replacement).and_return(expected_replacement)
|
|
268
|
+
allow(described_class).to receive(:preprocess_self_closing_tags).with(expected_replacement).and_return(expected_replacement)
|
|
269
|
+
|
|
270
|
+
# Mock JS components to avoid actual parsing
|
|
271
|
+
js_mock = double('JS')
|
|
272
|
+
allow(js_mock).to receive(:eval).and_return(mock_parser)
|
|
273
|
+
allow(js_mock).to receive(:try_convert).with(expected_replacement).and_return(expected_replacement)
|
|
274
|
+
stub_const('JS', js_mock)
|
|
275
|
+
|
|
276
|
+
allow(mock_parser).to receive(:call).and_return(mock_document)
|
|
277
|
+
allow(mock_document).to receive(:getElementsByTagName).and_return([mock_body])
|
|
278
|
+
allow(mock_body).to receive(:[]).and_return(mock_child_nodes)
|
|
279
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
|
|
280
|
+
|
|
281
|
+
described_class.parse(template)
|
|
282
|
+
|
|
283
|
+
# Verify that JS.try_convert was called with the replaced template
|
|
284
|
+
expect(js_mock).to have_received(:try_convert).with(expected_replacement)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it 'replaces <template attr="value"> with <div data-template attr="value">' do
|
|
288
|
+
template = '<template class="container"><div>content</div></template>'
|
|
289
|
+
expected_replacement = '<div data-template class="container"><div>content</div></div>'
|
|
290
|
+
|
|
291
|
+
# Mock the preprocessing chain
|
|
292
|
+
allow(described_class).to receive(:preprocess_template_tag).with(template).and_return(expected_replacement)
|
|
293
|
+
allow(described_class).to receive(:preprocess_pascal_case_component_name).with(expected_replacement).and_return(expected_replacement)
|
|
294
|
+
allow(described_class).to receive(:preprocess_self_closing_tags).with(expected_replacement).and_return(expected_replacement)
|
|
295
|
+
|
|
296
|
+
# Mock JS components to avoid actual parsing
|
|
297
|
+
js_mock = double('JS')
|
|
298
|
+
allow(js_mock).to receive(:eval).and_return(mock_parser)
|
|
299
|
+
allow(js_mock).to receive(:try_convert).with(expected_replacement).and_return(expected_replacement)
|
|
300
|
+
stub_const('JS', js_mock)
|
|
301
|
+
|
|
302
|
+
allow(mock_parser).to receive(:call).and_return(mock_document)
|
|
303
|
+
allow(mock_document).to receive(:getElementsByTagName).and_return([mock_body])
|
|
304
|
+
allow(mock_body).to receive(:[]).and_return(mock_child_nodes)
|
|
305
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(0)
|
|
306
|
+
|
|
307
|
+
described_class.parse(template)
|
|
308
|
+
|
|
309
|
+
# Verify that JS.try_convert was called with the replaced template
|
|
310
|
+
expect(js_mock).to have_received(:try_convert).with(expected_replacement)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
describe '.parse_and_eval' do
|
|
316
|
+
let(:mock_parser) { double('parser') }
|
|
317
|
+
let(:mock_document) { double('document') }
|
|
318
|
+
let(:mock_body) { double('body') }
|
|
319
|
+
let(:mock_child_nodes) { double('child_nodes') }
|
|
320
|
+
let(:mock_element) { double('element') }
|
|
321
|
+
let(:mock_vdom) { double('vdom') }
|
|
322
|
+
|
|
323
|
+
before do
|
|
324
|
+
# Mock preprocessing chain
|
|
325
|
+
allow(described_class).to receive(:preprocess_template_tag).with(anything).and_return('after_template_processing')
|
|
326
|
+
allow(described_class).to receive(:preprocess_pascal_case_component_name).with(anything).and_return('after_pascal_processing')
|
|
327
|
+
allow(described_class).to receive(:preprocess_self_closing_tags).with(anything).and_return('processed_template')
|
|
328
|
+
|
|
329
|
+
# Mock JS.eval, DOMParser, and JS.global
|
|
330
|
+
js_mock = double('JS')
|
|
331
|
+
allow(js_mock).to receive(:eval).with('return new DOMParser()').and_return(mock_parser)
|
|
332
|
+
allow(js_mock).to receive(:try_convert).with('processed_template').and_return('processed_template')
|
|
333
|
+
allow(js_mock).to receive(:global).and_return({ Node: { TEXT_NODE: 3, ELEMENT_NODE: 1 } })
|
|
334
|
+
stub_const('JS', js_mock)
|
|
335
|
+
|
|
336
|
+
allow(mock_parser).to receive(:call).with(:parseFromString, 'processed_template', 'text/html').and_return(mock_document)
|
|
337
|
+
allow(mock_document).to receive(:getElementsByTagName).with('body').and_return([mock_body])
|
|
338
|
+
allow(mock_body).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
|
|
339
|
+
|
|
340
|
+
# Mock element node
|
|
341
|
+
allow(mock_element).to receive(:[]).with(:nodeType).and_return(1) # ELEMENT_NODE
|
|
342
|
+
allow(mock_element).to receive(:[]).with(:tagName).and_return('DIV')
|
|
343
|
+
allow(mock_element).to receive(:[]).with(:attributes).and_return({ length: 0 })
|
|
344
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return([])
|
|
345
|
+
|
|
346
|
+
# Mock child nodes as a JavaScript-like array with length and indexing
|
|
347
|
+
mock_child_nodes_array = double('child_nodes_array')
|
|
348
|
+
allow(mock_child_nodes_array).to receive(:[]).with(:length).and_return(1)
|
|
349
|
+
allow(mock_child_nodes_array).to receive(:[]).with(0).and_return(mock_element)
|
|
350
|
+
allow(mock_body).to receive(:[]).with(:childNodes).and_return(mock_child_nodes_array)
|
|
351
|
+
|
|
352
|
+
# Mock empty child nodes for element
|
|
353
|
+
mock_empty_nodes = double('empty_nodes')
|
|
354
|
+
allow(mock_empty_nodes).to receive(:[]).with(:length).and_return(0)
|
|
355
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_empty_nodes)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
context 'when evaluating a template with variables' do
|
|
359
|
+
it 'returns a VDOM object with evaluated variables' do
|
|
360
|
+
template = '<div>{count}</div>'
|
|
361
|
+
count = 42 # rubocop:disable Lint/UselessAssignment
|
|
362
|
+
test_binding = binding()
|
|
363
|
+
|
|
364
|
+
# Mock element node
|
|
365
|
+
allow(mock_element).to receive(:[]).with(:nodeType).and_return(1) # ELEMENT_NODE
|
|
366
|
+
allow(mock_element).to receive(:[]).with(:tagName).and_return('DIV')
|
|
367
|
+
mock_element_attributes = double('element_attributes')
|
|
368
|
+
allow(mock_element_attributes).to receive(:to_a).and_return([])
|
|
369
|
+
allow(mock_element_attributes).to receive(:[]).with(:length).and_return(0)
|
|
370
|
+
allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_element_attributes)
|
|
371
|
+
|
|
372
|
+
# Mock text node for count
|
|
373
|
+
mock_text_node = double('text_node')
|
|
374
|
+
allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3) # TEXT_NODE
|
|
375
|
+
allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('{count}')
|
|
376
|
+
mock_child_nodes = double('child_nodes')
|
|
377
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(1)
|
|
378
|
+
allow(mock_child_nodes).to receive(:[]).with(0).and_return(mock_text_node)
|
|
379
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
|
|
380
|
+
|
|
381
|
+
# Mock RubyWasmUi::Vdom
|
|
382
|
+
stub_const('RubyWasmUi::Vdom', mock_vdom)
|
|
383
|
+
allow(mock_vdom).to receive(:h).with('div', {}, ["42"]).and_return(mock_vdom)
|
|
384
|
+
allow(mock_vdom).to receive(:h_fragment).with([mock_vdom]).and_return(mock_vdom)
|
|
385
|
+
|
|
386
|
+
result = described_class.parse_and_eval(template, test_binding)
|
|
387
|
+
expect(result).to eq(mock_vdom)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
context 'when evaluating a template with event handlers' do
|
|
392
|
+
it 'returns a VDOM object with event handlers' do
|
|
393
|
+
template = '<div on="{click: ->(e) { handle_click.call(e) }}"></div>'
|
|
394
|
+
handle_click = -> { 'clicked' } # rubocop:disable Lint/UselessAssignment
|
|
395
|
+
test_binding = binding()
|
|
396
|
+
|
|
397
|
+
# Mock element node with attributes
|
|
398
|
+
allow(mock_element).to receive(:[]).with(:nodeType).and_return(1) # ELEMENT_NODE
|
|
399
|
+
allow(mock_element).to receive(:[]).with(:tagName).and_return('DIV')
|
|
400
|
+
mock_attributes = double('attributes')
|
|
401
|
+
allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
|
|
402
|
+
mock_attribute = double('attribute')
|
|
403
|
+
allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
|
|
404
|
+
allow(mock_attribute).to receive(:[]).with(:name).and_return('on')
|
|
405
|
+
allow(mock_attribute).to receive(:[]).with(:value).and_return('{click: ->(e) { handle_click.call(e) }}')
|
|
406
|
+
allow(mock_attributes).to receive(:to_a).and_return([mock_attribute])
|
|
407
|
+
allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
|
|
408
|
+
|
|
409
|
+
# Mock RubyWasmUi::Vdom
|
|
410
|
+
stub_const('RubyWasmUi::Vdom', mock_vdom)
|
|
411
|
+
allow(mock_vdom).to receive(:h).with('div', { on: { click: kind_of(Proc) } }, []).and_return(mock_vdom)
|
|
412
|
+
allow(mock_vdom).to receive(:h_fragment).with([mock_vdom]).and_return(mock_vdom)
|
|
413
|
+
|
|
414
|
+
result = described_class.parse_and_eval(template, test_binding)
|
|
415
|
+
expect(result).to eq(mock_vdom)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
context 'when evaluating a template with components' do
|
|
420
|
+
let(:mock_component) { double('component') }
|
|
421
|
+
|
|
422
|
+
it 'returns a VDOM object with components' do
|
|
423
|
+
template = '<custom-component>{count}</custom-component>'
|
|
424
|
+
count = 42 # rubocop:disable Lint/UselessAssignment
|
|
425
|
+
test_binding = binding()
|
|
426
|
+
|
|
427
|
+
# Mock component node
|
|
428
|
+
allow(mock_element).to receive(:[]).with(:nodeType).and_return(1) # ELEMENT_NODE
|
|
429
|
+
allow(mock_element).to receive(:[]).with(:tagName).and_return('CUSTOM-COMPONENT')
|
|
430
|
+
mock_component_attributes = double('component_attributes')
|
|
431
|
+
allow(mock_component_attributes).to receive(:to_a).and_return([])
|
|
432
|
+
allow(mock_component_attributes).to receive(:[]).with(:length).and_return(0)
|
|
433
|
+
allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_component_attributes)
|
|
434
|
+
|
|
435
|
+
# Mock text node for count
|
|
436
|
+
mock_text_node = double('text_node')
|
|
437
|
+
allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3) # TEXT_NODE
|
|
438
|
+
allow(mock_text_node).to receive(:[]).with(:nodeValue).and_return('{count}')
|
|
439
|
+
mock_child_nodes = double('child_nodes')
|
|
440
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(1)
|
|
441
|
+
allow(mock_child_nodes).to receive(:[]).with(0).and_return(mock_text_node)
|
|
442
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
|
|
443
|
+
|
|
444
|
+
# Mock component class
|
|
445
|
+
stub_const('CustomComponent', mock_component)
|
|
446
|
+
|
|
447
|
+
# Mock RubyWasmUi::Vdom
|
|
448
|
+
stub_const('RubyWasmUi::Vdom', mock_vdom)
|
|
449
|
+
allow(mock_vdom).to receive(:h).with(mock_component, {}, ["42"]).and_return(mock_vdom)
|
|
450
|
+
allow(mock_vdom).to receive(:h_fragment).with([mock_vdom]).and_return(mock_vdom)
|
|
451
|
+
|
|
452
|
+
result = described_class.parse_and_eval(template, test_binding)
|
|
453
|
+
expect(result).to eq(mock_vdom)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
context 'when evaluating multiple top-level expressions' do
|
|
458
|
+
it 'wraps multiple expressions in a fragment' do
|
|
459
|
+
# Mock parse to return multiple expressions
|
|
460
|
+
allow(described_class).to receive(:parse).and_return('expr1,expr2')
|
|
461
|
+
|
|
462
|
+
# Mock eval to return the fragment
|
|
463
|
+
mock_fragment = double('fragment')
|
|
464
|
+
allow(described_class).to receive(:eval).with('RubyWasmUi::Vdom.h_fragment([expr1,expr2])', anything).and_return(mock_fragment)
|
|
465
|
+
|
|
466
|
+
result = described_class.parse_and_eval('<div>1</div><div>2</div>', binding)
|
|
467
|
+
expect(result).to eq(mock_fragment)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it 'wraps expressions containing end, in a fragment' do
|
|
471
|
+
# Mock parse to return expressions with 'end,'
|
|
472
|
+
allow(described_class).to receive(:parse).and_return('if condition then expr1 end,expr2')
|
|
473
|
+
|
|
474
|
+
# Mock eval to return the fragment
|
|
475
|
+
mock_fragment = double('fragment')
|
|
476
|
+
allow(described_class).to receive(:eval).with('RubyWasmUi::Vdom.h_fragment([if condition then expr1 end,expr2])', anything).and_return(mock_fragment)
|
|
477
|
+
|
|
478
|
+
result = described_class.parse_and_eval('<div r-if="{true}">content</div><div>other</div>', binding)
|
|
479
|
+
expect(result).to eq(mock_fragment)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
it 'does not wrap single expression' do
|
|
483
|
+
# Mock parse to return single expression
|
|
484
|
+
allow(described_class).to receive(:parse).and_return('single_expr')
|
|
485
|
+
|
|
486
|
+
# Mock eval to return the expression directly
|
|
487
|
+
mock_result = double('result')
|
|
488
|
+
allow(described_class).to receive(:eval).with('single_expr', anything).and_return(mock_result)
|
|
489
|
+
|
|
490
|
+
result = described_class.parse_and_eval('<div>single</div>', binding)
|
|
491
|
+
expect(result).to eq(mock_result)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
describe 'integration tests for preprocessing chain' do
|
|
497
|
+
context 'when processing PascalCase components with self-closing tags' do
|
|
498
|
+
it 'converts PascalCase to kebab-case then handles self-closing tags' do
|
|
499
|
+
template = '<SearchField placeholder="test" />'
|
|
500
|
+
|
|
501
|
+
# This should first convert to kebab-case, then handle self-closing
|
|
502
|
+
result = described_class.preprocess_pascal_case_component_name(template)
|
|
503
|
+
expect(result).to eq('<search-field placeholder="test" />')
|
|
504
|
+
|
|
505
|
+
# Then the self-closing tag should be processed
|
|
506
|
+
final_result = described_class.preprocess_self_closing_tags(result)
|
|
507
|
+
expect(final_result).to eq('<search-field placeholder="test" ></search-field>')
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it 'handles complex PascalCase components with attributes' do
|
|
511
|
+
template = '<TodoItemEditComponent r-if="{editing}" edited="{value}" on="{ save: ->() { save() } }" />'
|
|
512
|
+
|
|
513
|
+
# First convert to kebab-case
|
|
514
|
+
result = described_class.preprocess_pascal_case_component_name(template)
|
|
515
|
+
expect(result).to eq('<todo-item-edit-component r-if="{editing}" edited="{value}" on="{ save: ->() { save() } }" />')
|
|
516
|
+
|
|
517
|
+
# Then handle self-closing tag
|
|
518
|
+
final_result = described_class.preprocess_self_closing_tags(result)
|
|
519
|
+
expect(final_result).to eq('<todo-item-edit-component r-if="{editing}" edited="{value}" on="{ save: ->() { save() } }" ></todo-item-edit-component>')
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
it 'handles template tags with PascalCase components' do
|
|
523
|
+
template = '<template><ButtonComponent>Click me</ButtonComponent></template>'
|
|
524
|
+
|
|
525
|
+
# First process template tag
|
|
526
|
+
result = described_class.preprocess_template_tag(template)
|
|
527
|
+
expect(result).to eq('<div data-template><ButtonComponent>Click me</ButtonComponent></div>')
|
|
528
|
+
|
|
529
|
+
# Then convert PascalCase
|
|
530
|
+
result = described_class.preprocess_pascal_case_component_name(result)
|
|
531
|
+
expect(result).to eq('<div data-template><button-component>Click me</button-component></div>')
|
|
532
|
+
|
|
533
|
+
# Self-closing processing doesn't affect this case
|
|
534
|
+
final_result = described_class.preprocess_self_closing_tags(result)
|
|
535
|
+
expect(final_result).to eq('<div data-template><button-component>Click me</button-component></div>')
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
it 'handles PascalCase components with r-for attributes' do
|
|
539
|
+
template = '<TodoItemComponent r-for="{todo in todos}" key="{todo[:text]}" />'
|
|
540
|
+
|
|
541
|
+
# First convert to kebab-case
|
|
542
|
+
result = described_class.preprocess_pascal_case_component_name(template)
|
|
543
|
+
expect(result).to eq('<todo-item-component r-for="{todo in todos}" key="{todo[:text]}" />')
|
|
544
|
+
|
|
545
|
+
# Then handle self-closing tag
|
|
546
|
+
final_result = described_class.preprocess_self_closing_tags(result)
|
|
547
|
+
expect(final_result).to eq('<todo-item-component r-for="{todo in todos}" key="{todo[:text]}" ></todo-item-component>')
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
context 'when processing r-for attributes in templates' do
|
|
552
|
+
let(:mock_parser) { double('parser') }
|
|
553
|
+
let(:mock_document) { double('document') }
|
|
554
|
+
let(:mock_body) { double('body') }
|
|
555
|
+
let(:mock_child_nodes) { double('child_nodes') }
|
|
556
|
+
let(:mock_element) { double('element') }
|
|
557
|
+
let(:mock_attributes) { double('attributes') }
|
|
558
|
+
let(:mock_for_attribute) { double('for_attribute') }
|
|
559
|
+
let(:mock_key_attribute) { double('key_attribute') }
|
|
560
|
+
|
|
561
|
+
before do
|
|
562
|
+
# Mock preprocessing chain
|
|
563
|
+
allow(described_class).to receive(:preprocess_template_tag).with(anything).and_return('after_template_processing')
|
|
564
|
+
allow(described_class).to receive(:preprocess_pascal_case_component_name).with(anything).and_return('after_pascal_processing')
|
|
565
|
+
allow(described_class).to receive(:preprocess_self_closing_tags).with(anything).and_return('processed_template')
|
|
566
|
+
|
|
567
|
+
# Mock JS.eval, DOMParser, and JS.global
|
|
568
|
+
js_mock = double('JS')
|
|
569
|
+
allow(js_mock).to receive(:eval).with('return new DOMParser()').and_return(mock_parser)
|
|
570
|
+
allow(js_mock).to receive(:try_convert).with('processed_template').and_return('processed_template')
|
|
571
|
+
allow(js_mock).to receive(:global).and_return({ Node: { TEXT_NODE: 3, ELEMENT_NODE: 1 } })
|
|
572
|
+
stub_const('JS', js_mock)
|
|
573
|
+
|
|
574
|
+
allow(mock_parser).to receive(:call).with(:parseFromString, 'processed_template', 'text/html').and_return(mock_document)
|
|
575
|
+
allow(mock_document).to receive(:getElementsByTagName).with('body').and_return([mock_body])
|
|
576
|
+
allow(mock_body).to receive(:[]).with(:childNodes).and_return(mock_child_nodes)
|
|
577
|
+
|
|
578
|
+
# Mock element with r-for attribute
|
|
579
|
+
allow(mock_element).to receive(:[]).with(:nodeType).and_return(1) # ELEMENT_NODE
|
|
580
|
+
allow(mock_element).to receive(:[]).with(:tagName).and_return('TODO-ITEM-COMPONENT')
|
|
581
|
+
allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
|
|
582
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return([])
|
|
583
|
+
|
|
584
|
+
# Mock r-for and key attributes
|
|
585
|
+
allow(mock_attributes).to receive(:[]).with(:length).and_return(2)
|
|
586
|
+
allow(mock_attributes).to receive(:[]).with(0).and_return(mock_for_attribute)
|
|
587
|
+
allow(mock_attributes).to receive(:[]).with(1).and_return(mock_key_attribute)
|
|
588
|
+
allow(mock_for_attribute).to receive(:[]).with(:name).and_return('r-for')
|
|
589
|
+
allow(mock_for_attribute).to receive(:[]).with(:value).and_return('{todo in todos}')
|
|
590
|
+
allow(mock_key_attribute).to receive(:[]).with(:name).and_return('key')
|
|
591
|
+
allow(mock_key_attribute).to receive(:[]).with(:value).and_return('{todo[:text]}')
|
|
592
|
+
|
|
593
|
+
# Mock child nodes as a JavaScript-like array with length and indexing
|
|
594
|
+
allow(mock_child_nodes).to receive(:[]).with(:length).and_return(1)
|
|
595
|
+
allow(mock_child_nodes).to receive(:[]).with(0).and_return(mock_element)
|
|
596
|
+
|
|
597
|
+
# Mock empty child nodes for element
|
|
598
|
+
mock_empty_nodes = double('empty_nodes')
|
|
599
|
+
allow(mock_empty_nodes).to receive(:[]).with(:length).and_return(0)
|
|
600
|
+
allow(mock_element).to receive(:[]).with(:childNodes).and_return(mock_empty_nodes)
|
|
601
|
+
|
|
602
|
+
# Mock BuildForGroup and BuildConditionalGroup
|
|
603
|
+
allow(RubyWasmUi::Template::BuildConditionalGroup).to receive(:has_conditional_attribute?).with(mock_element).and_return(false)
|
|
604
|
+
allow(RubyWasmUi::Template::BuildForGroup).to receive(:has_for_attribute?).with(mock_element).and_return(true)
|
|
605
|
+
allow(RubyWasmUi::Template::BuildForGroup).to receive(:build_for_loop).with(mock_element).and_return("todos.map do |todo|\n RubyWasmUi::Vdom.h(TodoItemComponent, {:key => todo[:text]}, [])\nend")
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
it 'processes r-for attributes and generates map code' do
|
|
609
|
+
template = '<ul><TodoItemComponent r-for="{todo in todos}" key="{todo[:text]}" /></ul>'
|
|
610
|
+
|
|
611
|
+
result = described_class.parse(template)
|
|
612
|
+
expected = "*todos.map do |todo|\n RubyWasmUi::Vdom.h(TodoItemComponent, {:key => todo[:text]}, [])\nend"
|
|
613
|
+
expect(result).to eq(expected)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
it 'evaluates r-for templates with variables' do
|
|
617
|
+
template = '<ul><TodoItemComponent r-for="{todo in todos}" key="{todo[:text]}" /></ul>'
|
|
618
|
+
|
|
619
|
+
# Just test that parse returns the expected code structure
|
|
620
|
+
result = described_class.parse(template)
|
|
621
|
+
expect(result).to include('*todos.map do |todo|')
|
|
622
|
+
expect(result).to include('RubyWasmUi::Vdom.h(TodoItemComponent')
|
|
623
|
+
expect(result).to include('end')
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|