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,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Utils::Arrays do
6
+ describe '.without_nulls' do
7
+ context 'Basic functionality' do
8
+ it 'removes nil values from an array containing nils' do
9
+ arr = [1, nil, 2, nil, 3]
10
+ result = described_class.without_nulls(arr)
11
+ expect(result).to eq([1, 2, 3])
12
+ end
13
+
14
+ it 'returns the original array when it contains no nil values' do
15
+ arr = [1, 2, 3]
16
+ result = described_class.without_nulls(arr)
17
+ expect(result).to eq([1, 2, 3])
18
+ end
19
+ end
20
+
21
+ context 'Edge cases' do
22
+ it 'returns an empty array when given an empty array' do
23
+ arr = []
24
+ result = described_class.without_nulls(arr)
25
+ expect(result).to eq([])
26
+ end
27
+
28
+ it 'returns an empty array when all elements are nil' do
29
+ arr = [nil, nil, nil]
30
+ result = described_class.without_nulls(arr)
31
+ expect(result).to eq([])
32
+ end
33
+
34
+ it 'preserves falsy values like false, 0, and empty string' do
35
+ arr = [false, 0, '', nil]
36
+ result = described_class.without_nulls(arr)
37
+ expect(result).to eq([false, 0, ''])
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '.diff' do
43
+ context 'Basic functionality' do
44
+ it 'correctly detects added and removed elements' do
45
+ old_array = [1, 2, 3]
46
+ new_array = [2, 3, 4]
47
+ result = described_class.diff(old_array, new_array)
48
+ expect(result).to eq({
49
+ added: [4],
50
+ removed: [1]
51
+ })
52
+ end
53
+
54
+ it 'returns empty arrays when comparing identical arrays' do
55
+ array = [1, 2, 3]
56
+ result = described_class.diff(array, array)
57
+ expect(result).to eq({
58
+ added: [],
59
+ removed: []
60
+ })
61
+ end
62
+ end
63
+
64
+ context 'Edge cases' do
65
+ it 'works correctly when comparing with an empty array' do
66
+ old_array = []
67
+ new_array = [1, 2, 3]
68
+ result = described_class.diff(old_array, new_array)
69
+ expect(result).to eq({
70
+ added: [1, 2, 3],
71
+ removed: []
72
+ })
73
+ end
74
+
75
+ it 'handles duplicate elements correctly' do
76
+ old_array = [1, 1, 2, 2]
77
+ new_array = [2, 2, 3, 3]
78
+ result = described_class.diff(old_array, new_array)
79
+ expect(result).to eq({
80
+ added: [3],
81
+ removed: [1]
82
+ })
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '.diff_sequence' do
88
+ context 'Basic functionality' do
89
+ it 'returns correct sequence for simple addition' do
90
+ old_array = [1, 2, 3]
91
+ new_array = [1, 2, 3, 4]
92
+ result = described_class.diff_sequence(old_array, new_array)
93
+ expect(result).to eq([
94
+ { op: 'noop', original_index: 0, index: 0, item: 1 },
95
+ { op: 'noop', original_index: 1, index: 1, item: 2 },
96
+ { op: 'noop', original_index: 2, index: 2, item: 3 },
97
+ { op: 'add', index: 3, item: 4 }
98
+ ])
99
+ end
100
+
101
+ it 'returns correct sequence for simple removal' do
102
+ old_array = [1, 2, 3, 4]
103
+ new_array = [1, 2, 3]
104
+ result = described_class.diff_sequence(old_array, new_array)
105
+ expect(result).to eq([
106
+ { op: 'noop', original_index: 0, index: 0, item: 1 },
107
+ { op: 'noop', original_index: 1, index: 1, item: 2 },
108
+ { op: 'noop', original_index: 2, index: 2, item: 3 },
109
+ { op: 'remove', index: 3, item: 4 }
110
+ ])
111
+ end
112
+
113
+ it 'returns correct sequence for reordering' do
114
+ old_array = [1, 2, 3]
115
+ new_array = [3, 1, 2]
116
+ result = described_class.diff_sequence(old_array, new_array)
117
+ expect(result).to eq([
118
+ { op: 'move', original_index: 2, from: 2, index: 0, item: 3 },
119
+ { op: 'noop', original_index: 0, index: 1, item: 1 },
120
+ { op: 'noop', original_index: 1, index: 2, item: 2 }
121
+ ])
122
+ end
123
+ end
124
+
125
+ context 'Complex scenarios' do
126
+ it 'handles multiple operations in sequence' do
127
+ old_array = [1, 2, 3, 4]
128
+ new_array = [5, 2, 1, 6]
129
+ result = described_class.diff_sequence(old_array, new_array)
130
+ expect(result).to eq([
131
+ { op: 'add', index: 0, item: 5 },
132
+ { op: 'move', original_index: 1, from: 2, index: 1, item: 2 },
133
+ { op: 'noop', original_index: 0, index: 2, item: 1 },
134
+ { op: 'remove', index: 3, item: 3 },
135
+ { op: 'remove', index: 3, item: 4 },
136
+ { op: 'add', index: 3, item: 6 }
137
+ ])
138
+ end
139
+
140
+ it 'handles empty arrays' do
141
+ old_array = []
142
+ new_array = []
143
+ result = described_class.diff_sequence(old_array, new_array)
144
+ expect(result).to eq([])
145
+ end
146
+
147
+ it 'handles complete replacement' do
148
+ old_array = ['X', 'A', 'A', 'B', 'C']
149
+ new_array = ['C', 'K', 'A', 'B']
150
+
151
+ result = described_class.diff_sequence(old_array, new_array)
152
+ expect(result).to eq([
153
+ { op: 'remove', index: 0, item: 'X' },
154
+ { op: 'move', original_index: 4, from: 3, index: 0, item: 'C' },
155
+ { op: 'add', index: 1, item: 'K' },
156
+ { op: 'noop', original_index: 1, index: 2, item: 'A' },
157
+ { op: 'move', original_index: 3, from: 4, index: 3, item: 'B' },
158
+ { op: 'remove', index: 4, item: 'A' }
159
+ ])
160
+ end
161
+ end
162
+
163
+ context 'Custom equal_proc' do
164
+ it 'uses custom equal_proc for comparison' do
165
+ # ハッシュのidフィールドで比較するカスタム比較関数
166
+ equal_proc = ->(a, b) { a[:id] == b[:id] }
167
+
168
+ old_array = [
169
+ { id: 1, name: 'Alice' },
170
+ { id: 2, name: 'Bob' },
171
+ { id: 3, name: 'Charlie' }
172
+ ]
173
+ new_array = [
174
+ { id: 1, name: 'Alice Updated' }, # 名前が変わったが同じid
175
+ { id: 3, name: 'Charlie' },
176
+ { id: 2, name: 'Bob' }
177
+ ]
178
+
179
+ result = described_class.diff_sequence(old_array, new_array, equal_proc)
180
+ expect(result).to eq([
181
+ { op: 'noop', original_index: 0, index: 0, item: { id: 1, name: 'Alice' } },
182
+ { op: 'move', original_index: 2, from: 2, index: 1, item: { id: 3, name: 'Charlie' } },
183
+ { op: 'noop', original_index: 1, index: 2, item: { id: 2, name: 'Bob' } }
184
+ ])
185
+ end
186
+
187
+ it 'works with case-insensitive string comparison' do
188
+ equal_proc = ->(a, b) { a.downcase == b.downcase }
189
+
190
+ old_array = ['Hello', 'World', 'Test']
191
+ new_array = ['HELLO', 'test', 'New']
192
+
193
+ result = described_class.diff_sequence(old_array, new_array, equal_proc)
194
+ expect(result).to eq([
195
+ { op: 'noop', original_index: 0, index: 0, item: 'Hello' },
196
+ { op: 'remove', index: 1, item: 'World' },
197
+ { op: 'noop', original_index: 2, index: 1, item: 'Test' },
198
+ { op: 'add', index: 2, item: 'New' }
199
+ ])
200
+ end
201
+
202
+ it 'handles numeric comparison with tolerance' do
203
+ equal_proc = ->(a, b) { (a - b).abs < 0.1 }
204
+
205
+ old_array = [1.0, 2.0, 3.0]
206
+ new_array = [1.05, 3.02, 2.98] # 許容誤差内で同じとみなされる値
207
+
208
+ result = described_class.diff_sequence(old_array, new_array, equal_proc)
209
+ expect(result).to eq([
210
+ { op: 'noop', original_index: 0, index: 0, item: 1.0 },
211
+ { op: 'remove', index: 1, item: 2.0 },
212
+ { op: 'noop', original_index: 2, index: 1, item: 3.0 },
213
+ { op: 'add', index: 2, item: 2.98 }
214
+ ])
215
+ end
216
+
217
+ it 'defaults to standard equality when equal_proc is not provided' do
218
+ old_array = [1, 2, 3]
219
+ new_array = [1, 3, 2]
220
+
221
+ result_with_default = described_class.diff_sequence(old_array, new_array)
222
+ result_with_explicit = described_class.diff_sequence(old_array, new_array, ->(a, b) { a == b })
223
+
224
+ expect(result_with_default).to eq(result_with_explicit)
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Utils::Objects do
6
+ describe '.diff' do
7
+ context 'Basic diff calculation' do
8
+ it 'correctly detects added keys' do
9
+ old_obj = { a: 1, b: 2 }
10
+ new_obj = { a: 1, b: 2, c: 3 }
11
+
12
+ result = described_class.diff(old_obj, new_obj)
13
+
14
+ expect(result[:added]).to eq([:c])
15
+ expect(result[:removed]).to be_empty
16
+ expect(result[:updated]).to be_empty
17
+ end
18
+
19
+ it 'correctly detects removed keys' do
20
+ old_obj = { a: 1, b: 2, c: 3 }
21
+ new_obj = { a: 1, b: 2 }
22
+
23
+ result = described_class.diff(old_obj, new_obj)
24
+
25
+ expect(result[:added]).to be_empty
26
+ expect(result[:removed]).to eq([:c])
27
+ expect(result[:updated]).to be_empty
28
+ end
29
+
30
+ it 'correctly detects updated keys' do
31
+ old_obj = { a: 1, b: 2, c: 3 }
32
+ new_obj = { a: 1, b: 3, c: 3 }
33
+
34
+ result = described_class.diff(old_obj, new_obj)
35
+
36
+ expect(result[:added]).to be_empty
37
+ expect(result[:removed]).to be_empty
38
+ expect(result[:updated]).to eq([:b])
39
+ end
40
+ end
41
+
42
+ context 'Edge cases' do
43
+ it 'works correctly with empty objects' do
44
+ old_obj = {}
45
+ new_obj = { a: 1 }
46
+
47
+ result = described_class.diff(old_obj, new_obj)
48
+
49
+ expect(result[:added]).to eq([:a])
50
+ expect(result[:removed]).to be_empty
51
+ expect(result[:updated]).to be_empty
52
+ end
53
+
54
+ it 'works correctly with completely different objects' do
55
+ old_obj = { a: 1, b: 2 }
56
+ new_obj = { c: 3, d: 4 }
57
+
58
+ result = described_class.diff(old_obj, new_obj)
59
+
60
+ expect(result[:added]).to eq([:c, :d])
61
+ expect(result[:removed]).to eq([:a, :b])
62
+ expect(result[:updated]).to be_empty
63
+ end
64
+
65
+ it 'shows no differences when comparing the same object' do
66
+ obj = { a: 1, b: 2 }
67
+
68
+ result = described_class.diff(obj, obj)
69
+
70
+ expect(result[:added]).to be_empty
71
+ expect(result[:removed]).to be_empty
72
+ expect(result[:updated]).to be_empty
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '.has_own_property' do
78
+ context 'with Hash objects' do
79
+ let(:hash_obj) { { name: 'test', age: 25, 'city' => 'Tokyo' } }
80
+
81
+ it 'returns true when the key exists (symbol)' do
82
+ expect(described_class.has_own_property(hash_obj, :name)).to be true
83
+ end
84
+
85
+ it 'returns true when the key exists (string)' do
86
+ expect(described_class.has_own_property(hash_obj, 'city')).to be true
87
+ end
88
+
89
+ it 'returns false when the key does not exist' do
90
+ expect(described_class.has_own_property(hash_obj, :email)).to be false
91
+ end
92
+
93
+ it 'returns false when the key does not exist (string)' do
94
+ expect(described_class.has_own_property(hash_obj, 'country')).to be false
95
+ end
96
+
97
+ it 'works correctly with empty hash' do
98
+ empty_hash = {}
99
+ expect(described_class.has_own_property(empty_hash, :any_key)).to be false
100
+ end
101
+ end
102
+
103
+ context 'with custom objects' do
104
+ let(:custom_object) do
105
+ obj = Object.new
106
+ obj.instance_variable_set(:@name, 'test')
107
+ obj.instance_variable_set(:@age, 25)
108
+ obj
109
+ end
110
+
111
+ it 'returns true when the instance variable exists' do
112
+ expect(described_class.has_own_property(custom_object, :name)).to be true
113
+ expect(described_class.has_own_property(custom_object, 'age')).to be true
114
+ end
115
+
116
+ it 'returns false when the instance variable does not exist' do
117
+ expect(described_class.has_own_property(custom_object, :email)).to be false
118
+ expect(described_class.has_own_property(custom_object, 'city')).to be false
119
+ end
120
+
121
+ it 'works correctly with object without instance variables' do
122
+ empty_object = Object.new
123
+ expect(described_class.has_own_property(empty_object, :any_property)).to be false
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Utils::Props do
6
+ describe '.extract_props_and_events' do
7
+ context 'when vdom has props with events using symbol key' do
8
+ it 'separates props and events correctly' do
9
+ click_handler = proc { puts 'clicked' }
10
+ vdom = RubyWasmUi::Vdom.new(
11
+ 'div',
12
+ {
13
+ :on => { :click => click_handler },
14
+ :class => 'container',
15
+ :id => 'main'
16
+ },
17
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
18
+ [],
19
+ nil
20
+ )
21
+
22
+ result = described_class.extract_props_and_events(vdom)
23
+
24
+ expect(result[:props]).to eq({ :class => 'container', :id => 'main' })
25
+ expect(result[:events]).to eq({ :click => click_handler })
26
+ end
27
+ end
28
+
29
+ context 'when vdom has props with events using string key' do
30
+ it 'separates props and events correctly' do
31
+ click_handler = proc { puts 'clicked' }
32
+ vdom = RubyWasmUi::Vdom.new(
33
+ 'div',
34
+ {
35
+ "on" => { :click => click_handler },
36
+ :class => 'container',
37
+ :id => 'main'
38
+ },
39
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
40
+ [],
41
+ nil
42
+ )
43
+
44
+ result = described_class.extract_props_and_events(vdom)
45
+
46
+ expect(result[:props]).to eq({ :class => 'container', :id => 'main' })
47
+ expect(result[:events]).to eq({ :click => click_handler })
48
+ end
49
+ end
50
+
51
+ context 'when vdom has props without events' do
52
+ it 'returns all props as props and empty events' do
53
+ vdom = RubyWasmUi::Vdom.new(
54
+ 'div',
55
+ {
56
+ :class => 'container',
57
+ :id => 'main',
58
+ :style => 'color: red'
59
+ },
60
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
61
+ [],
62
+ nil
63
+ )
64
+
65
+ result = described_class.extract_props_and_events(vdom)
66
+
67
+ expect(result[:props]).to eq({
68
+ :class => 'container',
69
+ :id => 'main',
70
+ :style => 'color: red'
71
+ })
72
+ expect(result[:events]).to eq({})
73
+ end
74
+ end
75
+
76
+ context 'when vdom has empty props' do
77
+ it 'returns empty props and events' do
78
+ vdom = RubyWasmUi::Vdom.new(
79
+ 'div',
80
+ {},
81
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
82
+ [],
83
+ nil
84
+ )
85
+
86
+ result = described_class.extract_props_and_events(vdom)
87
+
88
+ expect(result[:props]).to eq({})
89
+ expect(result[:events]).to eq({})
90
+ end
91
+ end
92
+
93
+ context 'when vdom.props is nil' do
94
+ it 'returns empty props and events' do
95
+ vdom = RubyWasmUi::Vdom.new(
96
+ 'div',
97
+ nil,
98
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
99
+ [],
100
+ nil
101
+ )
102
+
103
+ result = described_class.extract_props_and_events(vdom)
104
+
105
+ expect(result[:props]).to eq({})
106
+ expect(result[:events]).to eq({})
107
+ end
108
+ end
109
+
110
+ context 'when vdom is nil' do
111
+ it 'returns empty props and events' do
112
+ result = described_class.extract_props_and_events(nil)
113
+
114
+ expect(result[:props]).to eq({})
115
+ expect(result[:events]).to eq({})
116
+ end
117
+ end
118
+
119
+ context 'when vdom has props with both symbol and string keys' do
120
+ it 'prioritizes symbol key for events' do
121
+ symbol_click_handler = proc { puts 'symbol click' }
122
+ string_hover_handler = proc { puts 'string hover' }
123
+ vdom = RubyWasmUi::Vdom.new(
124
+ 'div',
125
+ {
126
+ :on => { :click => symbol_click_handler },
127
+ "on" => { :hover => string_hover_handler },
128
+ :class => 'container'
129
+ },
130
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
131
+ [],
132
+ nil
133
+ )
134
+
135
+ result = described_class.extract_props_and_events(vdom)
136
+
137
+ expect(result[:props]).to eq({ :class => 'container' })
138
+ expect(result[:events]).to eq({ :click => symbol_click_handler })
139
+ end
140
+ end
141
+
142
+ context 'when vdom has props with only on events' do
143
+ it 'returns only events and empty props' do
144
+ click_handler = proc { puts 'clicked' }
145
+ hover_handler = proc { puts 'hovered' }
146
+ vdom = RubyWasmUi::Vdom.new(
147
+ 'div',
148
+ {
149
+ :on => {
150
+ :click => click_handler,
151
+ :hover => hover_handler
152
+ }
153
+ },
154
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
155
+ [],
156
+ nil
157
+ )
158
+
159
+ result = described_class.extract_props_and_events(vdom)
160
+
161
+ expect(result[:props]).to eq({})
162
+ expect(result[:events]).to eq({
163
+ :click => click_handler,
164
+ :hover => hover_handler
165
+ })
166
+ end
167
+ end
168
+
169
+ context 'when vdom has complex props structure' do
170
+ it 'correctly separates complex props and events' do
171
+ click_handler = proc { puts 'clicked' }
172
+ mouseover_handler = proc { puts 'mouseover' }
173
+ vdom = RubyWasmUi::Vdom.new(
174
+ 'div',
175
+ {
176
+ :on => {
177
+ :click => click_handler,
178
+ :mouseover => mouseover_handler
179
+ },
180
+ :class => ['container', 'active'],
181
+ :style => { :color => 'red', :fontSize => '16px' },
182
+ :data_testid => 'my-component',
183
+ :aria_label => 'Interactive element'
184
+ },
185
+ RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT],
186
+ [],
187
+ nil
188
+ )
189
+
190
+ result = described_class.extract_props_and_events(vdom)
191
+
192
+ expect(result[:props]).to eq({
193
+ :class => ['container', 'active'],
194
+ :style => { :color => 'red', :fontSize => '16px' },
195
+ :data_testid => 'my-component',
196
+ :aria_label => 'Interactive element'
197
+ })
198
+ expect(result[:events]).to eq({
199
+ :click => click_handler,
200
+ :mouseover => mouseover_handler
201
+ })
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyWasmUi::Utils::Strings do
6
+ describe '.is_not_empty_string' do
7
+ context 'Basic functionality' do
8
+ it 'returns true for non-empty strings' do
9
+ result = described_class.is_not_empty_string('hello')
10
+ expect(result).to be true
11
+ end
12
+
13
+ it 'returns false for empty strings' do
14
+ result = described_class.is_not_empty_string('')
15
+ expect(result).to be false
16
+ end
17
+
18
+ it 'returns true for strings with only spaces' do
19
+ result = described_class.is_not_empty_string(' ')
20
+ expect(result).to be true
21
+ end
22
+
23
+ it 'returns true for strings with special characters' do
24
+ result = described_class.is_not_empty_string('!@#$%')
25
+ expect(result).to be true
26
+ end
27
+ end
28
+
29
+ context 'Edge cases' do
30
+ it 'returns true for single character strings' do
31
+ result = described_class.is_not_empty_string('a')
32
+ expect(result).to be true
33
+ end
34
+
35
+ it 'returns true for strings with newlines' do
36
+ result = described_class.is_not_empty_string("\n")
37
+ expect(result).to be true
38
+ end
39
+
40
+ it 'returns true for strings with tabs' do
41
+ result = described_class.is_not_empty_string("\t")
42
+ expect(result).to be true
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '.is_not_blank_or_empty_string' do
48
+ context 'Basic functionality' do
49
+ it 'returns true for non-empty strings' do
50
+ result = described_class.is_not_blank_or_empty_string('hello')
51
+ expect(result).to be true
52
+ end
53
+
54
+ it 'returns false for empty strings' do
55
+ result = described_class.is_not_blank_or_empty_string('')
56
+ expect(result).to be false
57
+ end
58
+
59
+ it 'returns false for strings with only spaces' do
60
+ result = described_class.is_not_blank_or_empty_string(' ')
61
+ expect(result).to be false
62
+ end
63
+
64
+ it 'returns true for strings with content and leading/trailing spaces' do
65
+ result = described_class.is_not_blank_or_empty_string(' hello ')
66
+ expect(result).to be true
67
+ end
68
+
69
+ it 'returns true for strings with special characters' do
70
+ result = described_class.is_not_blank_or_empty_string('!@#$%')
71
+ expect(result).to be true
72
+ end
73
+ end
74
+
75
+ context 'Edge cases' do
76
+ it 'returns false for strings with only newlines' do
77
+ result = described_class.is_not_blank_or_empty_string("\n\n")
78
+ expect(result).to be false
79
+ end
80
+
81
+ it 'returns false for strings with only tabs' do
82
+ result = described_class.is_not_blank_or_empty_string("\t\t")
83
+ expect(result).to be false
84
+ end
85
+
86
+ it 'returns false for strings with mixed whitespace characters' do
87
+ result = described_class.is_not_blank_or_empty_string(" \t\n ")
88
+ expect(result).to be false
89
+ end
90
+
91
+ it 'returns true for single non-whitespace character' do
92
+ result = described_class.is_not_blank_or_empty_string('a')
93
+ expect(result).to be true
94
+ end
95
+
96
+ it 'returns true for strings with content surrounded by mixed whitespace' do
97
+ result = described_class.is_not_blank_or_empty_string(" \t hello \n ")
98
+ expect(result).to be true
99
+ end
100
+
101
+ it 'returns true for Unicode whitespace (strip does not remove Unicode whitespace)' do
102
+ result = described_class.is_not_blank_or_empty_string("\u00A0\u2000\u2001")
103
+ expect(result).to be true
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ Dir[File.expand_path('../../src/**/*.rb', __FILE__)].sort.each { |f| require f }
5
+
6
+ RSpec.configure do |config|
7
+ config.expect_with :rspec do |expectations|
8
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
9
+ end
10
+
11
+ config.mock_with :rspec do |mocks|
12
+ mocks.verify_partial_doubles = true
13
+ end
14
+
15
+ config.shared_context_metadata_behavior = :apply_to_host_groups
16
+ end
@@ -0,0 +1,5 @@
1
+ import { expect, test } from "vitest";
2
+
3
+ test("sample test", () => {
4
+ expect(1).toBe(1);
5
+ });