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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::NodesEqual do
6
+ describe '.equal?' do
7
+ context 'Nodes with different types' do
8
+ it 'returns false for nodes with different types' do
9
+ text_node = RubyWasmUi::Vdom.h_string('Hello')
10
+ element_node = RubyWasmUi::Vdom.h('div')
11
+
12
+ result = described_class.equal?(text_node, element_node)
13
+ expect(result).to be false
14
+ end
15
+
16
+ it 'returns false for TEXT and ELEMENT type nodes' do
17
+ text_node = RubyWasmUi::Vdom.h_string('Hello')
18
+ element_node = RubyWasmUi::Vdom.h('span')
19
+
20
+ result = described_class.equal?(text_node, element_node)
21
+ expect(result).to be false
22
+ end
23
+
24
+ it 'returns false for ELEMENT and FRAGMENT type nodes' do
25
+ element_node = RubyWasmUi::Vdom.h('div')
26
+ fragment_node = RubyWasmUi::Vdom.h_fragment([])
27
+
28
+ result = described_class.equal?(element_node, fragment_node)
29
+ expect(result).to be false
30
+ end
31
+ end
32
+
33
+ context 'ELEMENT type nodes' do
34
+ it 'returns true for ELEMENT nodes with the same tag name' do
35
+ node_one = RubyWasmUi::Vdom.h('div')
36
+ node_two = RubyWasmUi::Vdom.h('div')
37
+
38
+ result = described_class.equal?(node_one, node_two)
39
+ expect(result).to be true
40
+ end
41
+
42
+ it 'returns false for ELEMENT nodes with different tag names' do
43
+ node_one = RubyWasmUi::Vdom.h('div')
44
+ node_two = RubyWasmUi::Vdom.h('span')
45
+
46
+ result = described_class.equal?(node_one, node_two)
47
+ expect(result).to be false
48
+ end
49
+
50
+ it 'returns false for ELEMENT nodes with different cases in tag names' do
51
+ node_one = RubyWasmUi::Vdom.h('DIV')
52
+ node_two = RubyWasmUi::Vdom.h('div')
53
+
54
+ result = described_class.equal?(node_one, node_two)
55
+ expect(result).to be false
56
+ end
57
+
58
+ it 'returns true for ELEMENT nodes with complex tag names' do
59
+ node_one = RubyWasmUi::Vdom.h('custom-element')
60
+ node_two = RubyWasmUi::Vdom.h('custom-element')
61
+
62
+ result = described_class.equal?(node_one, node_two)
63
+ expect(result).to be true
64
+ end
65
+
66
+ it 'returns true for ELEMENT nodes with the same tag name and same key' do
67
+ node_one = RubyWasmUi::Vdom.h('div', { key: 'test-key' })
68
+ node_two = RubyWasmUi::Vdom.h('div', { key: 'test-key' })
69
+
70
+ result = described_class.equal?(node_one, node_two)
71
+ expect(result).to be true
72
+ end
73
+
74
+ it 'returns false for ELEMENT nodes with the same tag name but different keys' do
75
+ node_one = RubyWasmUi::Vdom.h('div', { key: 'key1' })
76
+ node_two = RubyWasmUi::Vdom.h('div', { key: 'key2' })
77
+
78
+ result = described_class.equal?(node_one, node_two)
79
+ expect(result).to be false
80
+ end
81
+
82
+ it 'returns false for ELEMENT nodes with one having key and other having no key' do
83
+ node_one = RubyWasmUi::Vdom.h('div', { key: 'test-key' })
84
+ node_two = RubyWasmUi::Vdom.h('div')
85
+
86
+ result = described_class.equal?(node_one, node_two)
87
+ expect(result).to be false
88
+ end
89
+
90
+ it 'returns true for ELEMENT nodes with both having nil keys' do
91
+ node_one = RubyWasmUi::Vdom.h('div', { key: nil })
92
+ node_two = RubyWasmUi::Vdom.h('div', { key: nil })
93
+
94
+ result = described_class.equal?(node_one, node_two)
95
+ expect(result).to be true
96
+ end
97
+ end
98
+
99
+ context 'COMPONENT type nodes' do
100
+ let(:dummy_component) { Class.new }
101
+ let(:another_component) { Class.new }
102
+
103
+ it 'returns true for COMPONENT nodes with the same component class' do
104
+ node_one = RubyWasmUi::Vdom.h(dummy_component)
105
+ node_two = RubyWasmUi::Vdom.h(dummy_component)
106
+
107
+ result = described_class.equal?(node_one, node_two)
108
+ expect(result).to be true
109
+ end
110
+
111
+ it 'returns false for COMPONENT nodes with different component classes' do
112
+ node_one = RubyWasmUi::Vdom.h(dummy_component)
113
+ node_two = RubyWasmUi::Vdom.h(another_component)
114
+
115
+ result = described_class.equal?(node_one, node_two)
116
+ expect(result).to be false
117
+ end
118
+
119
+ it 'returns true for COMPONENT nodes with the same component class and same key' do
120
+ node_one = RubyWasmUi::Vdom.h(dummy_component, { key: 'test-key' })
121
+ node_two = RubyWasmUi::Vdom.h(dummy_component, { key: 'test-key' })
122
+
123
+ result = described_class.equal?(node_one, node_two)
124
+ expect(result).to be true
125
+ end
126
+
127
+ it 'returns false for COMPONENT nodes with the same component class but different keys' do
128
+ node_one = RubyWasmUi::Vdom.h(dummy_component, { key: 'key1' })
129
+ node_two = RubyWasmUi::Vdom.h(dummy_component, { key: 'key2' })
130
+
131
+ result = described_class.equal?(node_one, node_two)
132
+ expect(result).to be false
133
+ end
134
+
135
+ it 'returns false for COMPONENT nodes with one having key and other having no key' do
136
+ node_one = RubyWasmUi::Vdom.h(dummy_component, { key: 'test-key' })
137
+ node_two = RubyWasmUi::Vdom.h(dummy_component)
138
+
139
+ result = described_class.equal?(node_one, node_two)
140
+ expect(result).to be false
141
+ end
142
+ end
143
+
144
+ context 'TEXT type nodes' do
145
+ it 'returns true for TEXT nodes with the same value' do
146
+ node_one = RubyWasmUi::Vdom.h_string('Hello')
147
+ node_two = RubyWasmUi::Vdom.h_string('Hello')
148
+
149
+ result = described_class.equal?(node_one, node_two)
150
+ expect(result).to be true
151
+ end
152
+
153
+ it 'returns false for TEXT nodes with different values' do
154
+ node_one = RubyWasmUi::Vdom.h_string('Hello')
155
+ node_two = RubyWasmUi::Vdom.h_string('World')
156
+
157
+ result = described_class.equal?(node_one, node_two)
158
+ expect(result).to be false
159
+ end
160
+ end
161
+
162
+ context 'FRAGMENT type nodes' do
163
+ it 'returns true for FRAGMENT nodes' do
164
+ node_one = RubyWasmUi::Vdom.h_fragment([])
165
+ node_two = RubyWasmUi::Vdom.h_fragment([])
166
+
167
+ result = described_class.equal?(node_one, node_two)
168
+ expect(result).to be true
169
+ end
170
+ end
171
+
172
+ context 'Edge cases' do
173
+ it 'returns true for ELEMENT nodes with empty string tag names' do
174
+ node_one = RubyWasmUi::Vdom.h('')
175
+ node_two = RubyWasmUi::Vdom.h('')
176
+
177
+ result = described_class.equal?(node_one, node_two)
178
+ expect(result).to be true
179
+ end
180
+
181
+ it 'returns false for ELEMENT nodes with empty and non-empty tag names' do
182
+ node_one = RubyWasmUi::Vdom.h('')
183
+ node_two = RubyWasmUi::Vdom.h('div')
184
+
185
+ result = described_class.equal?(node_one, node_two)
186
+ expect(result).to be false
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,505 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Template::BuildConditionalGroup 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
+ describe '.has_conditional_attribute?' do
18
+ let(:mock_element) { double('element') }
19
+ let(:mock_attributes) { double('attributes') }
20
+
21
+ context 'when element has r-if attribute' do
22
+ before do
23
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
24
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
25
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
26
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-if')
27
+ end
28
+
29
+ let(:mock_attribute) { double('attribute') }
30
+
31
+ it 'returns true' do
32
+ result = described_class.has_conditional_attribute?(mock_element)
33
+ expect(result).to be true
34
+ end
35
+ end
36
+
37
+ context 'when element has r-elsif attribute' do
38
+ before do
39
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
40
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
41
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
42
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-elsif')
43
+ end
44
+
45
+ let(:mock_attribute) { double('attribute') }
46
+
47
+ it 'returns true' do
48
+ result = described_class.has_conditional_attribute?(mock_element)
49
+ expect(result).to be true
50
+ end
51
+ end
52
+
53
+ context 'when element has r-else attribute' do
54
+ before do
55
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
56
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
57
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
58
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-else')
59
+ end
60
+
61
+ let(:mock_attribute) { double('attribute') }
62
+
63
+ it 'returns true' do
64
+ result = described_class.has_conditional_attribute?(mock_element)
65
+ expect(result).to be true
66
+ end
67
+ end
68
+
69
+ context 'when element has no conditional attributes' do
70
+ before do
71
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
72
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
73
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
74
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
75
+ end
76
+
77
+ let(:mock_attribute) { double('attribute') }
78
+
79
+ it 'returns false' do
80
+ result = described_class.has_conditional_attribute?(mock_element)
81
+ expect(result).to be false
82
+ end
83
+ end
84
+
85
+ context 'when element has no attributes' do
86
+ before do
87
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(nil)
88
+ end
89
+
90
+ it 'returns false' do
91
+ result = described_class.has_conditional_attribute?(mock_element)
92
+ expect(result).to be false
93
+ end
94
+ end
95
+ end
96
+
97
+ describe '.build_conditional_group' do
98
+ let(:mock_elements) { double('elements') }
99
+ let(:mock_element_if) { double('element_if') }
100
+ let(:mock_element_elsif) { double('element_elsif') }
101
+ let(:mock_element_else) { double('element_else') }
102
+ let(:mock_attributes_if) { double('attributes_if') }
103
+ let(:mock_attributes_elsif) { double('attributes_elsif') }
104
+ let(:mock_attributes_else) { double('attributes_else') }
105
+ let(:mock_attribute_if) { double('attribute_if') }
106
+ let(:mock_attribute_elsif) { double('attribute_elsif') }
107
+ let(:mock_attribute_else) { double('attribute_else') }
108
+
109
+ context 'with complete conditional chain (r-if, r-elsif, r-else)' do
110
+ before do
111
+ # Mock elements array
112
+ allow(mock_elements).to receive(:[]).with(:length).and_return(3)
113
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_element_if)
114
+ allow(mock_elements).to receive(:[]).with(1).and_return(mock_element_elsif)
115
+ allow(mock_elements).to receive(:[]).with(2).and_return(mock_element_else)
116
+
117
+ # Mock element types
118
+ allow(mock_element_if).to receive(:[]).with(:nodeType).and_return(1)
119
+ allow(mock_element_elsif).to receive(:[]).with(:nodeType).and_return(1)
120
+ allow(mock_element_else).to receive(:[]).with(:nodeType).and_return(1)
121
+
122
+ # Mock has_conditional_attribute? responses
123
+ allow(described_class).to receive(:has_conditional_attribute?)
124
+ .with(mock_element_if).and_return(true)
125
+ allow(described_class).to receive(:has_conditional_attribute?)
126
+ .with(mock_element_elsif).and_return(true)
127
+ allow(described_class).to receive(:has_conditional_attribute?)
128
+ .with(mock_element_else).and_return(true)
129
+
130
+ # Mock get_conditional_type responses
131
+ allow(described_class).to receive(:get_conditional_type)
132
+ .with(mock_element_if).and_return('r-if')
133
+ allow(described_class).to receive(:get_conditional_type)
134
+ .with(mock_element_elsif).and_return('r-elsif')
135
+ allow(described_class).to receive(:get_conditional_type)
136
+ .with(mock_element_else).and_return('r-else')
137
+
138
+ # Mock get_conditional_expression responses
139
+ allow(described_class).to receive(:get_conditional_expression)
140
+ .with(mock_element_if).and_return('condition1')
141
+ allow(described_class).to receive(:get_conditional_expression)
142
+ .with(mock_element_elsif).and_return('condition2')
143
+ allow(described_class).to receive(:get_conditional_expression)
144
+ .with(mock_element_else).and_return('true')
145
+
146
+ # Mock build_single_conditional_content responses
147
+ allow(described_class).to receive(:build_single_conditional_content)
148
+ .with(mock_element_if).and_return('content_if')
149
+ allow(described_class).to receive(:build_single_conditional_content)
150
+ .with(mock_element_elsif).and_return('content_elsif')
151
+ allow(described_class).to receive(:build_single_conditional_content)
152
+ .with(mock_element_else).and_return('content_else')
153
+ end
154
+
155
+ it 'builds complete conditional chain' do
156
+ result, next_index = described_class.build_conditional_group(mock_elements, 0)
157
+
158
+ expected = [
159
+ 'if condition1',
160
+ ' content_if',
161
+ 'elsif condition2',
162
+ ' content_elsif',
163
+ 'else',
164
+ ' content_else',
165
+ 'end'
166
+ ].join("\n")
167
+
168
+ expect(result).to eq(expected)
169
+ expect(next_index).to eq(3)
170
+ end
171
+ end
172
+
173
+ context 'with only r-if (no r-else)' do
174
+ before do
175
+ # Mock elements array
176
+ allow(mock_elements).to receive(:[]).with(:length).and_return(1)
177
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_element_if)
178
+
179
+ # Mock element type
180
+ allow(mock_element_if).to receive(:[]).with(:nodeType).and_return(1)
181
+
182
+ # Mock has_conditional_attribute? response
183
+ allow(described_class).to receive(:has_conditional_attribute?)
184
+ .with(mock_element_if).and_return(true)
185
+
186
+ # Mock get_conditional_type response
187
+ allow(described_class).to receive(:get_conditional_type)
188
+ .with(mock_element_if).and_return('r-if')
189
+
190
+ # Mock get_conditional_expression response
191
+ allow(described_class).to receive(:get_conditional_expression)
192
+ .with(mock_element_if).and_return('condition')
193
+
194
+ # Mock build_single_conditional_content response
195
+ allow(described_class).to receive(:build_single_conditional_content)
196
+ .with(mock_element_if).and_return('content')
197
+ end
198
+
199
+ it 'builds conditional with default else clause' do
200
+ result, next_index = described_class.build_conditional_group(mock_elements, 0)
201
+
202
+ expected = [
203
+ 'if condition',
204
+ ' content',
205
+ 'else',
206
+ ' RubyWasmUi::Vdom.h_fragment([])',
207
+ 'end'
208
+ ].join("\n")
209
+
210
+ expect(result).to eq(expected)
211
+ expect(next_index).to eq(1)
212
+ end
213
+ end
214
+
215
+ context 'with text nodes between elements' do
216
+ let(:mock_text_node) { double('text_node') }
217
+
218
+ before do
219
+ # Mock elements array with text node
220
+ allow(mock_elements).to receive(:[]).with(:length).and_return(3)
221
+ allow(mock_elements).to receive(:[]).with(0).and_return(mock_element_if)
222
+ allow(mock_elements).to receive(:[]).with(1).and_return(mock_text_node)
223
+ allow(mock_elements).to receive(:[]).with(2).and_return(mock_element_else)
224
+
225
+ # Mock node types
226
+ allow(mock_element_if).to receive(:[]).with(:nodeType).and_return(1)
227
+ allow(mock_text_node).to receive(:[]).with(:nodeType).and_return(3)
228
+ allow(mock_element_else).to receive(:[]).with(:nodeType).and_return(1)
229
+
230
+ # Mock has_conditional_attribute? responses
231
+ allow(described_class).to receive(:has_conditional_attribute?)
232
+ .with(mock_element_if).and_return(true)
233
+ allow(described_class).to receive(:has_conditional_attribute?)
234
+ .with(mock_element_else).and_return(true)
235
+
236
+ # Mock get_conditional_type responses
237
+ allow(described_class).to receive(:get_conditional_type)
238
+ .with(mock_element_if).and_return('r-if')
239
+ allow(described_class).to receive(:get_conditional_type)
240
+ .with(mock_element_else).and_return('r-else')
241
+
242
+ # Mock get_conditional_expression response
243
+ allow(described_class).to receive(:get_conditional_expression)
244
+ .with(mock_element_if).and_return('condition')
245
+ allow(described_class).to receive(:get_conditional_expression)
246
+ .with(mock_element_else).and_return('true')
247
+
248
+ # Mock build_single_conditional_content responses
249
+ allow(described_class).to receive(:build_single_conditional_content)
250
+ .with(mock_element_if).and_return('content_if')
251
+ allow(described_class).to receive(:build_single_conditional_content)
252
+ .with(mock_element_else).and_return('content_else')
253
+ end
254
+
255
+ it 'skips text nodes and processes conditional elements' do
256
+ result, next_index = described_class.build_conditional_group(mock_elements, 0)
257
+
258
+ expected = [
259
+ 'if condition',
260
+ ' content_if',
261
+ 'else',
262
+ ' content_else',
263
+ 'end'
264
+ ].join("\n")
265
+
266
+ expect(result).to eq(expected)
267
+ expect(next_index).to eq(3)
268
+ end
269
+ end
270
+ end
271
+
272
+ describe '.get_conditional_type' do
273
+ let(:mock_element) { double('element') }
274
+ let(:mock_attributes) { double('attributes') }
275
+ let(:mock_attribute) { double('attribute') }
276
+
277
+ before do
278
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
279
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
280
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
281
+ end
282
+
283
+ context 'when element has r-if attribute' do
284
+ before do
285
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-if')
286
+ end
287
+
288
+ it 'returns r-if' do
289
+ result = described_class.get_conditional_type(mock_element)
290
+ expect(result).to eq('r-if')
291
+ end
292
+ end
293
+
294
+ context 'when element has r-elsif attribute' do
295
+ before do
296
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-elsif')
297
+ end
298
+
299
+ it 'returns r-elsif' do
300
+ result = described_class.get_conditional_type(mock_element)
301
+ expect(result).to eq('r-elsif')
302
+ end
303
+ end
304
+
305
+ context 'when element has r-else attribute' do
306
+ before do
307
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-else')
308
+ end
309
+
310
+ it 'returns r-else' do
311
+ result = described_class.get_conditional_type(mock_element)
312
+ expect(result).to eq('r-else')
313
+ end
314
+ end
315
+
316
+ context 'when element has no conditional attributes' do
317
+ before do
318
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
319
+ end
320
+
321
+ it 'returns r-if as fallback' do
322
+ result = described_class.get_conditional_type(mock_element)
323
+ expect(result).to eq('r-if')
324
+ end
325
+ end
326
+ end
327
+
328
+ describe '.get_conditional_expression' do
329
+ let(:mock_element) { double('element') }
330
+ let(:mock_attributes) { double('attributes') }
331
+ let(:mock_attribute) { double('attribute') }
332
+
333
+ before do
334
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
335
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(1)
336
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute)
337
+ end
338
+
339
+ context 'when element has r-if attribute with embedded script' do
340
+ before do
341
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-if')
342
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('{state[:visible]}')
343
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('{state[:visible]}').and_return(true)
344
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:get_embed_script).with('{state[:visible]}').and_return('state[:visible]')
345
+ end
346
+
347
+ it 'returns the extracted script' do
348
+ result = described_class.get_conditional_expression(mock_element)
349
+ expect(result).to eq('state[:visible]')
350
+ end
351
+ end
352
+
353
+ context 'when element has r-elsif attribute with plain value' do
354
+ before do
355
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('r-elsif')
356
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('condition')
357
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:embed_script?).with('condition').and_return(false)
358
+ end
359
+
360
+ it 'returns the plain value' do
361
+ result = described_class.get_conditional_expression(mock_element)
362
+ expect(result).to eq('condition')
363
+ end
364
+ end
365
+
366
+ context 'when element has no conditional attributes' do
367
+ before do
368
+ allow(mock_attribute).to receive(:[]).with(:name).and_return('class')
369
+ allow(mock_attribute).to receive(:[]).with(:value).and_return('some-class')
370
+ end
371
+
372
+ it 'returns true as fallback' do
373
+ result = described_class.get_conditional_expression(mock_element)
374
+ expect(result).to eq('true')
375
+ end
376
+ end
377
+ end
378
+
379
+ describe '.filter_conditional_attributes' do
380
+ let(:mock_attributes) { double('attributes') }
381
+ let(:mock_attribute_1) { double('attribute_1') }
382
+ let(:mock_attribute_2) { double('attribute_2') }
383
+ let(:mock_attribute_3) { double('attribute_3') }
384
+ let(:mock_attribute_4) { double('attribute_4') }
385
+
386
+ context 'when attributes contain conditional and data-template attributes' do
387
+ before do
388
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(4)
389
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
390
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_attribute_2)
391
+ allow(mock_attributes).to receive(:[]).with(2).and_return(mock_attribute_3)
392
+ allow(mock_attributes).to receive(:[]).with(3).and_return(mock_attribute_4)
393
+
394
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('r-if')
395
+ allow(mock_attribute_2).to receive(:[]).with(:name).and_return('class')
396
+ allow(mock_attribute_3).to receive(:[]).with(:name).and_return('data-template')
397
+ allow(mock_attribute_4).to receive(:[]).with(:name).and_return('id')
398
+ end
399
+
400
+ it 'filters out conditional and data-template attributes' do
401
+ result = described_class.filter_conditional_attributes(mock_attributes)
402
+ expect(result).to eq([mock_attribute_2, mock_attribute_4])
403
+ end
404
+ end
405
+
406
+ context 'when attributes contain only regular attributes' do
407
+ before do
408
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(2)
409
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
410
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_attribute_2)
411
+
412
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('class')
413
+ allow(mock_attribute_2).to receive(:[]).with(:name).and_return('id')
414
+ end
415
+
416
+ it 'returns all attributes' do
417
+ result = described_class.filter_conditional_attributes(mock_attributes)
418
+ expect(result).to eq([mock_attribute_1, mock_attribute_2])
419
+ end
420
+ end
421
+
422
+ context 'when attributes contain only conditional attributes' do
423
+ before do
424
+ allow(mock_attributes).to receive(:[]).with(:length).and_return(3)
425
+ allow(mock_attributes).to receive(:[]).with(0).and_return(mock_attribute_1)
426
+ allow(mock_attributes).to receive(:[]).with(1).and_return(mock_attribute_2)
427
+ allow(mock_attributes).to receive(:[]).with(2).and_return(mock_attribute_3)
428
+
429
+ allow(mock_attribute_1).to receive(:[]).with(:name).and_return('r-if')
430
+ allow(mock_attribute_2).to receive(:[]).with(:name).and_return('r-elsif')
431
+ allow(mock_attribute_3).to receive(:[]).with(:name).and_return('r-else')
432
+ end
433
+
434
+ it 'returns empty array' do
435
+ result = described_class.filter_conditional_attributes(mock_attributes)
436
+ expect(result).to eq([])
437
+ end
438
+ end
439
+ end
440
+
441
+ describe '.build_single_conditional_content' do
442
+ let(:mock_element) { double('element') }
443
+ let(:mock_attributes) { double('attributes') }
444
+ let(:filtered_attributes) { [] }
445
+
446
+ before do
447
+ allow(mock_element).to receive(:[]).with(:attributes).and_return(mock_attributes)
448
+ allow(described_class).to receive(:filter_conditional_attributes).with(mock_attributes).and_return(filtered_attributes)
449
+ end
450
+
451
+ context 'when element is a template' do
452
+ before do
453
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('TEMPLATE')
454
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:has_data_template_attribute?).with(mock_element).and_return(false)
455
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build_fragment).with(mock_element, 'template').and_return('fragment_result')
456
+ end
457
+
458
+ it 'builds fragment' do
459
+ result = described_class.build_single_conditional_content(mock_element)
460
+ expect(result).to eq('fragment_result')
461
+ end
462
+ end
463
+
464
+ context 'when element is a div with data-template attribute' do
465
+ before do
466
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('DIV')
467
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:has_data_template_attribute?).with(mock_element).and_return(true)
468
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build_fragment).with(mock_element, 'div').and_return('fragment_result')
469
+ end
470
+
471
+ it 'builds fragment' do
472
+ result = described_class.build_single_conditional_content(mock_element)
473
+ expect(result).to eq('fragment_result')
474
+ end
475
+ end
476
+
477
+ context 'when element is a component' do
478
+ before do
479
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('CUSTOM-COMPONENT')
480
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:has_data_template_attribute?).with(mock_element).and_return(false)
481
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:is_component?).with('custom-component').and_return(true)
482
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build_component).with(mock_element, 'custom-component', filtered_attributes).and_return('component_result')
483
+ end
484
+
485
+ it 'builds component' do
486
+ result = described_class.build_single_conditional_content(mock_element)
487
+ expect(result).to eq('component_result')
488
+ end
489
+ end
490
+
491
+ context 'when element is a regular HTML element' do
492
+ before do
493
+ allow(mock_element).to receive(:[]).with(:tagName).and_return('DIV')
494
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:has_data_template_attribute?).with(mock_element).and_return(false)
495
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:is_component?).with('div').and_return(false)
496
+ allow(RubyWasmUi::Template::BuildVdom).to receive(:build_element).with(mock_element, 'div', filtered_attributes).and_return('element_result')
497
+ end
498
+
499
+ it 'builds element' do
500
+ result = described_class.build_single_conditional_content(mock_element)
501
+ expect(result).to eq('element_result')
502
+ end
503
+ end
504
+ end
505
+ end