ruwi 0.10.0
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 +31 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +56 -0
- data/README.md +237 -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/.gitignore +4 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +39 -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/examples/src/index.html +21 -0
- data/examples/src/index.rb +26 -0
- data/examples/src/todos/index.html +23 -0
- data/examples/src/todos/index.rb +237 -0
- data/examples/src/todos/todos_repository.rb +23 -0
- data/exe/ruwi +6 -0
- data/lib/ruwi/cli/command/base.rb +192 -0
- data/lib/ruwi/cli/command/dev.rb +207 -0
- data/lib/ruwi/cli/command/pack.rb +36 -0
- data/lib/ruwi/cli/command/rebuild.rb +38 -0
- data/lib/ruwi/cli/command/setup.rb +159 -0
- data/lib/ruwi/cli/command.rb +48 -0
- data/lib/ruwi/runtime/app.rb +53 -0
- data/lib/ruwi/runtime/component.rb +215 -0
- data/lib/ruwi/runtime/dispatcher.rb +46 -0
- data/lib/ruwi/runtime/dom/attributes.rb +105 -0
- data/lib/ruwi/runtime/dom/destroy_dom.rb +63 -0
- data/lib/ruwi/runtime/dom/events.rb +40 -0
- data/lib/ruwi/runtime/dom/mount_dom.rb +108 -0
- data/lib/ruwi/runtime/dom/patch_dom.rb +237 -0
- data/lib/ruwi/runtime/dom/scheduler.rb +51 -0
- data/lib/ruwi/runtime/dom.rb +13 -0
- data/lib/ruwi/runtime/nodes_equal.rb +45 -0
- data/lib/ruwi/runtime/template/build_conditional_group.rb +150 -0
- data/lib/ruwi/runtime/template/build_for_group.rb +125 -0
- data/lib/ruwi/runtime/template/build_vdom.rb +220 -0
- data/lib/ruwi/runtime/template/parser.rb +134 -0
- data/lib/ruwi/runtime/template.rb +11 -0
- data/lib/ruwi/runtime/utils/arrays.rb +185 -0
- data/lib/ruwi/runtime/utils/objects.rb +37 -0
- data/lib/ruwi/runtime/utils/props.rb +25 -0
- data/lib/ruwi/runtime/utils/strings.rb +19 -0
- data/lib/ruwi/runtime/utils.rb +11 -0
- data/lib/ruwi/runtime/vdom.rb +84 -0
- data/lib/ruwi/version.rb +5 -0
- data/lib/ruwi.rb +14 -0
- data/package-lock.json +73 -0
- data/package.json +32 -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 +147 -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/ruwi +1 -0
- data/packages/npm-packages/runtime/src/ruwi.rb +1 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruwi.rbs +4 -0
- data/spec/ruwi/cli/command/base_spec.rb +503 -0
- data/spec/ruwi/cli/command/dev_spec.rb +442 -0
- data/spec/ruwi/cli/command/pack_spec.rb +131 -0
- data/spec/ruwi/cli/command/rebuild_spec.rb +95 -0
- data/spec/ruwi/cli/command/setup_spec.rb +251 -0
- data/spec/ruwi/cli/command_spec.rb +118 -0
- data/spec/ruwi/runtime/component_spec.rb +416 -0
- data/spec/ruwi/runtime/dom/scheduler_spec.rb +98 -0
- data/spec/ruwi/runtime/nodes_equal_spec.rb +190 -0
- data/spec/ruwi/runtime/template/build_conditional_group_spec.rb +505 -0
- data/spec/ruwi/runtime/template/build_for_group_spec.rb +377 -0
- data/spec/ruwi/runtime/template/build_vdom_spec.rb +573 -0
- data/spec/ruwi/runtime/template/parser_spec.rb +627 -0
- data/spec/ruwi/runtime/utils/arrays_spec.rb +228 -0
- data/spec/ruwi/runtime/utils/objects_spec.rb +127 -0
- data/spec/ruwi/runtime/utils/props_spec.rb +205 -0
- data/spec/ruwi/runtime/utils/strings_spec.rb +107 -0
- data/spec/spec_helper.rb +16 -0
- metadata +229 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Ruwi do
|
|
6
|
+
describe '.define_component' do
|
|
7
|
+
let(:template) { ->(component) { 'rendered content' } }
|
|
8
|
+
let(:state) { ->(props) { { count: 0 } } }
|
|
9
|
+
|
|
10
|
+
context 'with template proc' do
|
|
11
|
+
it 'works with template proc that accepts component argument' do
|
|
12
|
+
component_class = Ruwi.define_component(
|
|
13
|
+
template: ->(component) { 'rendered with component' }
|
|
14
|
+
)
|
|
15
|
+
instance = component_class.new
|
|
16
|
+
expect(instance.template).to eq('rendered with component')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'works with template proc that accepts no arguments' do
|
|
20
|
+
component_class = Ruwi.define_component(
|
|
21
|
+
template: -> { 'rendered without args' }
|
|
22
|
+
)
|
|
23
|
+
instance = component_class.new
|
|
24
|
+
expect(instance.template).to eq('rendered without args')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'can access component state and props in template proc without arguments' do
|
|
28
|
+
component_class = Ruwi.define_component(
|
|
29
|
+
template: -> { "count: #{@state[:count]}, name: #{@props[:name]}" },
|
|
30
|
+
state: -> { { count: 5 } }
|
|
31
|
+
)
|
|
32
|
+
instance = component_class.new(name: 'test')
|
|
33
|
+
expect(instance.template).to eq('count: 5, name: test')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'can access component methods in template proc without arguments' do
|
|
37
|
+
component_class = Ruwi.define_component(
|
|
38
|
+
template: -> { helper_method },
|
|
39
|
+
methods: {
|
|
40
|
+
helper_method: -> { 'helper result' }
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
instance = component_class.new
|
|
44
|
+
expect(instance.template).to eq('helper result')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'passes correct component instance when template proc has arguments' do
|
|
48
|
+
received_component = nil
|
|
49
|
+
component_class = Ruwi.define_component(
|
|
50
|
+
template: ->(component) {
|
|
51
|
+
received_component = component
|
|
52
|
+
'rendered'
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
instance = component_class.new
|
|
56
|
+
instance.template
|
|
57
|
+
expect(received_component).to eq(instance)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'handles template proc with variable arity correctly' do
|
|
61
|
+
# Test with proc that can accept 0 or more arguments
|
|
62
|
+
component_class = Ruwi.define_component(
|
|
63
|
+
template: ->(*args) { "args count: #{args.length}" }
|
|
64
|
+
)
|
|
65
|
+
instance = component_class.new
|
|
66
|
+
# Variable arity procs (arity < 0) should be called with component argument
|
|
67
|
+
expect(instance.template).to eq('args count: 1')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'works with Vdom.h in template proc without arguments' do
|
|
71
|
+
component_class = Ruwi.define_component(
|
|
72
|
+
template: -> {
|
|
73
|
+
Ruwi::Vdom.h('div', {}, ["Hello #{@props[:name]}"])
|
|
74
|
+
},
|
|
75
|
+
state: -> { { count: 0 } }
|
|
76
|
+
)
|
|
77
|
+
instance = component_class.new(name: 'World')
|
|
78
|
+
result = instance.template
|
|
79
|
+
expect(result).to be_a(Ruwi::Vdom)
|
|
80
|
+
expect(result.tag).to eq('div')
|
|
81
|
+
expect(result.children.length).to eq(1)
|
|
82
|
+
expect(result.children.first).to be_a(Ruwi::Vdom)
|
|
83
|
+
expect(result.children.first.type).to eq('text')
|
|
84
|
+
expect(result.children.first.value).to eq('Hello World')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'works with Vdom.h in template proc with component argument' do
|
|
88
|
+
component_class = Ruwi.define_component(
|
|
89
|
+
template: ->(component) {
|
|
90
|
+
Ruwi::Vdom.h('div', {}, ["Count: #{component.state[:count]}"])
|
|
91
|
+
},
|
|
92
|
+
state: -> { { count: 42 } }
|
|
93
|
+
)
|
|
94
|
+
instance = component_class.new
|
|
95
|
+
result = instance.template
|
|
96
|
+
expect(result).to be_a(Ruwi::Vdom)
|
|
97
|
+
expect(result.tag).to eq('div')
|
|
98
|
+
expect(result.children.length).to eq(1)
|
|
99
|
+
expect(result.children.first).to be_a(Ruwi::Vdom)
|
|
100
|
+
expect(result.children.first.type).to eq('text')
|
|
101
|
+
expect(result.children.first.value).to eq('Count: 42')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'with state proc' do
|
|
106
|
+
it 'works with state proc that accepts props argument' do
|
|
107
|
+
component_class = Ruwi.define_component(
|
|
108
|
+
template: -> { 'content' },
|
|
109
|
+
state: ->(props) { { value: props[:initial] } }
|
|
110
|
+
)
|
|
111
|
+
instance = component_class.new(initial: 5)
|
|
112
|
+
expect(instance.state).to eq({ value: 5 })
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'works with state proc that accepts no arguments' do
|
|
116
|
+
component_class = Ruwi.define_component(
|
|
117
|
+
template: -> { 'content' },
|
|
118
|
+
state: -> { { value: 10 } }
|
|
119
|
+
)
|
|
120
|
+
instance = component_class.new
|
|
121
|
+
expect(instance.state).to eq({ value: 10 })
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'with methods parameter' do
|
|
126
|
+
it 'successfully adds custom methods to the component' do
|
|
127
|
+
custom_methods = {
|
|
128
|
+
increment: -> { @state[:count] += 1 },
|
|
129
|
+
get_double_count: -> { @state[:count] * 2 }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
component_class = Ruwi.define_component(
|
|
133
|
+
template:,
|
|
134
|
+
state:,
|
|
135
|
+
methods: custom_methods
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
instance = component_class.new
|
|
139
|
+
|
|
140
|
+
expect(instance).to respond_to(:increment)
|
|
141
|
+
expect(instance).to respond_to(:get_double_count)
|
|
142
|
+
|
|
143
|
+
# Test the custom methods work correctly
|
|
144
|
+
expect(instance.get_double_count).to eq(0)
|
|
145
|
+
instance.increment
|
|
146
|
+
expect(instance.state[:count]).to eq(1)
|
|
147
|
+
expect(instance.get_double_count).to eq(2)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'raises error when method name conflicts with existing method' do
|
|
151
|
+
custom_methods = {
|
|
152
|
+
template: -> { 'custom template' } # conflicts with existing template method
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
expect {
|
|
156
|
+
Ruwi.define_component(
|
|
157
|
+
template:,
|
|
158
|
+
methods: custom_methods
|
|
159
|
+
)
|
|
160
|
+
}.to raise_error(/Method "template\(\)" already exists in the component\./)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'raises error when method name conflicts with private method' do
|
|
164
|
+
custom_methods = {
|
|
165
|
+
patch: -> { 'custom patch' } # conflicts with existing private method
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect {
|
|
169
|
+
Ruwi.define_component(
|
|
170
|
+
template:,
|
|
171
|
+
methods: custom_methods
|
|
172
|
+
)
|
|
173
|
+
}.to raise_error(/Method "patch\(\)" already exists in the component\./)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'works correctly with empty methods hash' do
|
|
177
|
+
component_class = Ruwi.define_component(
|
|
178
|
+
template:,
|
|
179
|
+
methods: {}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
instance = component_class.new
|
|
183
|
+
expect(instance).to be_a(Ruwi::Component)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'allows method names as strings' do
|
|
187
|
+
custom_methods = {
|
|
188
|
+
'string_method' => -> { 'method called' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
component_class = Ruwi.define_component(
|
|
192
|
+
template:,
|
|
193
|
+
methods: custom_methods
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
instance = component_class.new
|
|
197
|
+
expect(instance).to respond_to('string_method')
|
|
198
|
+
expect(instance.string_method).to eq('method called')
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context 'without methods parameter' do
|
|
203
|
+
it 'creates component successfully when methods is not provided' do
|
|
204
|
+
component_class = Ruwi.define_component(template:)
|
|
205
|
+
|
|
206
|
+
instance = component_class.new
|
|
207
|
+
expect(instance).to be_a(Ruwi::Component)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
context 'with on_mounted parameter' do
|
|
212
|
+
it 'works with on_mounted proc that accepts component argument' do
|
|
213
|
+
mounted_called = false
|
|
214
|
+
received_component = nil
|
|
215
|
+
|
|
216
|
+
component_class = Ruwi.define_component(
|
|
217
|
+
template: -> { 'content' },
|
|
218
|
+
on_mounted: ->(component) {
|
|
219
|
+
mounted_called = true
|
|
220
|
+
received_component = component
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
instance = component_class.new
|
|
225
|
+
instance.on_mounted
|
|
226
|
+
|
|
227
|
+
expect(mounted_called).to be true
|
|
228
|
+
expect(received_component).to eq(instance)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'works with on_mounted proc that accepts no arguments' do
|
|
232
|
+
mounted_called = false
|
|
233
|
+
|
|
234
|
+
component_class = Ruwi.define_component(
|
|
235
|
+
template: -> { 'content' },
|
|
236
|
+
on_mounted: -> {
|
|
237
|
+
mounted_called = true
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
instance = component_class.new
|
|
242
|
+
instance.on_mounted
|
|
243
|
+
|
|
244
|
+
expect(mounted_called).to be true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'allows calling component methods directly in on_mounted without arguments' do
|
|
248
|
+
method_called_with_self = nil
|
|
249
|
+
|
|
250
|
+
component_class = Ruwi.define_component(
|
|
251
|
+
template: -> { 'content' },
|
|
252
|
+
on_mounted: -> {
|
|
253
|
+
method_called_with_self = self
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
instance = component_class.new
|
|
258
|
+
instance.on_mounted
|
|
259
|
+
|
|
260
|
+
expect(method_called_with_self).to eq(instance)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'uses default empty proc when on_mounted is not provided' do
|
|
264
|
+
component_class = Ruwi.define_component(template: -> { 'content' })
|
|
265
|
+
instance = component_class.new
|
|
266
|
+
|
|
267
|
+
expect { instance.on_mounted }.not_to raise_error
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
context 'with on_unmounted parameter' do
|
|
272
|
+
it 'works with on_unmounted proc that accepts component argument' do
|
|
273
|
+
unmounted_called = false
|
|
274
|
+
received_component = nil
|
|
275
|
+
|
|
276
|
+
component_class = Ruwi.define_component(
|
|
277
|
+
template: -> { 'content' },
|
|
278
|
+
on_unmounted: ->(component) {
|
|
279
|
+
unmounted_called = true
|
|
280
|
+
received_component = component
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
instance = component_class.new
|
|
285
|
+
instance.on_unmounted
|
|
286
|
+
|
|
287
|
+
expect(unmounted_called).to be true
|
|
288
|
+
expect(received_component).to eq(instance)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it 'works with on_unmounted proc that accepts no arguments' do
|
|
292
|
+
unmounted_called = false
|
|
293
|
+
|
|
294
|
+
component_class = Ruwi.define_component(
|
|
295
|
+
template: -> { 'content' },
|
|
296
|
+
on_unmounted: -> {
|
|
297
|
+
unmounted_called = true
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
instance = component_class.new
|
|
302
|
+
instance.on_unmounted
|
|
303
|
+
|
|
304
|
+
expect(unmounted_called).to be true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'allows calling component methods directly in on_unmounted without arguments' do
|
|
308
|
+
method_called_with_self = nil
|
|
309
|
+
|
|
310
|
+
component_class = Ruwi.define_component(
|
|
311
|
+
template: -> { 'content' },
|
|
312
|
+
on_unmounted: -> {
|
|
313
|
+
method_called_with_self = self
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
instance = component_class.new
|
|
318
|
+
instance.on_unmounted
|
|
319
|
+
|
|
320
|
+
expect(method_called_with_self).to eq(instance)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'uses default empty proc when on_unmounted is not provided' do
|
|
324
|
+
component_class = Ruwi.define_component(template: -> { 'content' })
|
|
325
|
+
instance = component_class.new
|
|
326
|
+
|
|
327
|
+
expect { instance.on_unmounted }.not_to raise_error
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
describe Ruwi::Component do
|
|
333
|
+
describe '#emit' do
|
|
334
|
+
let(:template) { -> { 'content' } }
|
|
335
|
+
let(:component_class) { Ruwi.define_component(template:) }
|
|
336
|
+
let(:event_name) { 'test_event' }
|
|
337
|
+
|
|
338
|
+
context 'when dispatcher is set' do
|
|
339
|
+
it 'dispatches event with payload' do
|
|
340
|
+
component = component_class.new
|
|
341
|
+
dispatcher = component.instance_variable_get(:@dispatcher)
|
|
342
|
+
payload = { value: 42 }
|
|
343
|
+
|
|
344
|
+
expect(dispatcher).to receive(:dispatch).with(event_name, payload)
|
|
345
|
+
component.emit(event_name, payload)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it 'dispatches event without payload (nil by default)' do
|
|
349
|
+
component = component_class.new
|
|
350
|
+
dispatcher = component.instance_variable_get(:@dispatcher)
|
|
351
|
+
|
|
352
|
+
expect(dispatcher).to receive(:dispatch).with(event_name, nil)
|
|
353
|
+
component.emit(event_name)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
context 'when dispatcher is not set' do
|
|
358
|
+
it 'does not raise error' do
|
|
359
|
+
component = component_class.new
|
|
360
|
+
component.instance_variable_set(:@dispatcher, nil)
|
|
361
|
+
|
|
362
|
+
expect { component.emit(event_name) }.not_to raise_error
|
|
363
|
+
expect { component.emit(event_name, { value: 42 }) }.not_to raise_error
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
describe '#wire_event_handler' do
|
|
369
|
+
let(:template) { -> { 'content' } }
|
|
370
|
+
let(:component_class) { Ruwi.define_component(template:) }
|
|
371
|
+
let(:event_name) { 'test_event' }
|
|
372
|
+
let(:parent_component) { component_class.new }
|
|
373
|
+
|
|
374
|
+
context 'with parent component' do
|
|
375
|
+
it 'handles event with payload when handler has arity of 1' do
|
|
376
|
+
handler = ->(payload) { payload[:value] * 2 }
|
|
377
|
+
component = component_class.new({}, { event_name => handler }, parent_component)
|
|
378
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
379
|
+
expect(subscription).to be_a(Proc)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it 'handles event without payload when handler has arity of 0' do
|
|
383
|
+
handler = -> { 'no payload' }
|
|
384
|
+
component = component_class.new({}, { event_name => handler }, parent_component)
|
|
385
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
386
|
+
expect(subscription).to be_a(Proc)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
context 'without parent component' do
|
|
391
|
+
it 'handles event with payload when handler has arity of 1' do
|
|
392
|
+
handler = ->(payload) { payload[:value] * 2 }
|
|
393
|
+
component = component_class.new({}, { event_name => handler })
|
|
394
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
395
|
+
expect(subscription).to be_a(Proc)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it 'handles event without payload when handler has arity of 0' do
|
|
399
|
+
handler = -> { 'no payload' }
|
|
400
|
+
component = component_class.new({}, { event_name => handler })
|
|
401
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
402
|
+
expect(subscription).to be_a(Proc)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'returns a no-op unsubscription when subscription is nil' do
|
|
407
|
+
allow_any_instance_of(Ruwi::Dispatcher).to receive(:subscribe).and_return(nil)
|
|
408
|
+
handler = -> { 'test' }
|
|
409
|
+
component = component_class.new({}, { event_name => handler })
|
|
410
|
+
subscription = component.send(:wire_event_handler, event_name, handler)
|
|
411
|
+
expect(subscription).to be_a(Proc)
|
|
412
|
+
expect { subscription.call }.not_to raise_error
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Ruwi::Dom::Scheduler do
|
|
6
|
+
let(:mock_job) { -> { 'job executed' } }
|
|
7
|
+
let(:js) { class_double('JS').as_stubbed_const }
|
|
8
|
+
let(:global) { double('JS.global') }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
allow(js).to receive(:global).and_return(global)
|
|
12
|
+
allow(global).to receive(:queueMicrotask)
|
|
13
|
+
described_class.initialize_scheduler
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '.initialize_scheduler' do
|
|
17
|
+
it 'initializes the scheduler with empty jobs and scheduled false' do
|
|
18
|
+
expect(described_class.jobs).to eq([])
|
|
19
|
+
expect(described_class.scheduled).to be false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.enqueue_job' do
|
|
24
|
+
context 'when jobs is nil' do
|
|
25
|
+
before do
|
|
26
|
+
described_class.jobs = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'initializes scheduler and enqueues the job' do
|
|
30
|
+
described_class.enqueue_job(mock_job)
|
|
31
|
+
expect(described_class.jobs).to eq([mock_job])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'when jobs is already initialized' do
|
|
36
|
+
it 'enqueues the job' do
|
|
37
|
+
described_class.enqueue_job(mock_job)
|
|
38
|
+
expect(described_class.jobs).to eq([mock_job])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'schedules an update' do
|
|
43
|
+
expect(global).to receive(:queueMicrotask)
|
|
44
|
+
described_class.enqueue_job(mock_job)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '.schedule_update' do
|
|
49
|
+
context 'when not already scheduled' do
|
|
50
|
+
before do
|
|
51
|
+
described_class.scheduled = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'sets scheduled to true and queues microtask' do
|
|
55
|
+
expect(global).to receive(:queueMicrotask)
|
|
56
|
+
described_class.send(:schedule_update)
|
|
57
|
+
expect(described_class.scheduled).to be true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'when already scheduled' do
|
|
62
|
+
before do
|
|
63
|
+
described_class.scheduled = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'does not queue microtask' do
|
|
67
|
+
expect(global).not_to receive(:queueMicrotask)
|
|
68
|
+
described_class.send(:schedule_update)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '.process_jobs' do
|
|
74
|
+
let(:job1) { spy('job1') }
|
|
75
|
+
let(:job2) { spy('job2') }
|
|
76
|
+
|
|
77
|
+
before do
|
|
78
|
+
described_class.jobs = [job1, job2]
|
|
79
|
+
described_class.scheduled = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'processes all jobs in the queue' do
|
|
83
|
+
described_class.send(:process_jobs)
|
|
84
|
+
expect(job1).to have_received(:call)
|
|
85
|
+
expect(job2).to have_received(:call)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'empties the jobs queue' do
|
|
89
|
+
described_class.send(:process_jobs)
|
|
90
|
+
expect(described_class.jobs).to be_empty
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'sets scheduled to false after processing' do
|
|
94
|
+
described_class.send(:process_jobs)
|
|
95
|
+
expect(described_class.scheduled).to be false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Ruwi::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 = Ruwi::Vdom.h_string('Hello')
|
|
10
|
+
element_node = Ruwi::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 = Ruwi::Vdom.h_string('Hello')
|
|
18
|
+
element_node = Ruwi::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 = Ruwi::Vdom.h('div')
|
|
26
|
+
fragment_node = Ruwi::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 = Ruwi::Vdom.h('div')
|
|
36
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('div')
|
|
44
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('DIV')
|
|
52
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('custom-element')
|
|
60
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('div', { key: 'test-key' })
|
|
68
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('div', { key: 'key1' })
|
|
76
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('div', { key: 'test-key' })
|
|
84
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('div', { key: nil })
|
|
92
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h(dummy_component)
|
|
105
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h(dummy_component)
|
|
113
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h(dummy_component, { key: 'test-key' })
|
|
121
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h(dummy_component, { key: 'key1' })
|
|
129
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h(dummy_component, { key: 'test-key' })
|
|
137
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h_string('Hello')
|
|
147
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h_string('Hello')
|
|
155
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h_fragment([])
|
|
165
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('')
|
|
175
|
+
node_two = Ruwi::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 = Ruwi::Vdom.h('')
|
|
183
|
+
node_two = Ruwi::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
|