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,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
|