neo.mjs 10.0.0-beta.4 → 10.0.0-beta.5
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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/datahandling/StateProviders.md +1 -0
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/tree.json +1 -0
- package/package.json +2 -2
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +226 -78
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +7 -2
- package/src/component/Base.mjs +67 -46
- package/src/container/Base.mjs +28 -24
- package/src/core/Base.mjs +138 -19
- package/src/core/Config.mjs +123 -32
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
@@ -0,0 +1,537 @@
|
|
1
|
+
import Neo from '../../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../../src/core/_export.mjs';
|
3
|
+
import InstanceManager from '../../../../src/manager/Instance.mjs';
|
4
|
+
import Component from '../../../../src/component/Base.mjs';
|
5
|
+
import StateProvider from '../../../../src/state/Provider.mjs';
|
6
|
+
import Store from '../../../../src/data/Store.mjs';
|
7
|
+
|
8
|
+
|
9
|
+
// Mock Component for testing purposes
|
10
|
+
class MockComponent extends Component {
|
11
|
+
static config = {
|
12
|
+
className : 'Mock.Component',
|
13
|
+
appName : 'test-app',
|
14
|
+
testConfig_: null
|
15
|
+
}
|
16
|
+
}
|
17
|
+
Neo.setupClass(MockComponent);
|
18
|
+
|
19
|
+
StartTest(t => {
|
20
|
+
// Helper function to convert a proxy to a plain object for deep comparison
|
21
|
+
function proxyToObject(proxy) {
|
22
|
+
return JSON.parse(JSON.stringify(proxy))
|
23
|
+
}
|
24
|
+
|
25
|
+
t.it('Provider should initialize with data and create configs', t => {
|
26
|
+
const component = Neo.create(MockComponent, {
|
27
|
+
stateProvider: {
|
28
|
+
data: {name: 'Test', value: 123, user: {firstName: 'John'}}
|
29
|
+
}
|
30
|
+
});
|
31
|
+
const provider = component.getStateProvider();
|
32
|
+
|
33
|
+
t.is(provider.getDataConfig('name').get(), 'Test', 'Name config should be created and have correct value');
|
34
|
+
t.is(provider.getDataConfig('value').get(), 123, 'Value config should be created and have correct value');
|
35
|
+
t.is(provider.getDataConfig('user.firstName').get(), 'John', 'Nested user.firstName config should be created');
|
36
|
+
|
37
|
+
component.destroy();
|
38
|
+
});
|
39
|
+
|
40
|
+
t.it('Provider should update data and trigger config changes', t => {
|
41
|
+
const component = Neo.create(MockComponent, {stateProvider: {data: {counter: 0}}});
|
42
|
+
const provider = component.getStateProvider();
|
43
|
+
|
44
|
+
let effectRunCount = 0;
|
45
|
+
provider.createBinding(component.id, 'testConfig', data => {
|
46
|
+
effectRunCount++;
|
47
|
+
return data.counter;
|
48
|
+
});
|
49
|
+
|
50
|
+
t.is(effectRunCount, 1, 'Binding effect should run once initially');
|
51
|
+
t.is(component.testConfig, 0, 'Component config should be initialized with data value');
|
52
|
+
|
53
|
+
component.setState('counter', 1);
|
54
|
+
t.is(effectRunCount, 2, 'Binding effect should re-run after setData');
|
55
|
+
t.is(component.testConfig, 1, 'Component config should be updated after setData');
|
56
|
+
|
57
|
+
component.setState({counter: 2});
|
58
|
+
t.is(effectRunCount, 3, 'Binding effect should re-run after setData with object');
|
59
|
+
t.is(component.testConfig, 2, 'Component config should be updated after setData with object');
|
60
|
+
|
61
|
+
component.destroy();
|
62
|
+
});
|
63
|
+
|
64
|
+
t.it('Provider should handle hierarchical data access', t => {
|
65
|
+
const parentComponent = Neo.create(MockComponent, {
|
66
|
+
stateProvider: {data: {appTitle: 'My App', user: {firstName: 'Parent'}}}
|
67
|
+
});
|
68
|
+
const childComponent = Neo.create(MockComponent, {
|
69
|
+
stateProvider: {data: {user: {lastName: 'Child'}}},
|
70
|
+
parentComponent: parentComponent
|
71
|
+
});
|
72
|
+
|
73
|
+
let effectRunCount = 0;
|
74
|
+
childComponent.getStateProvider().createBinding(childComponent.id, 'testConfig', data => {
|
75
|
+
effectRunCount++;
|
76
|
+
return `${data.appTitle} - ${data.user.firstName} ${data.user.lastName}`;
|
77
|
+
});
|
78
|
+
|
79
|
+
t.is(effectRunCount, 1, 'Binding effect should run once initially');
|
80
|
+
t.is(childComponent.testConfig, 'My App - Parent Child', 'Component config should reflect hierarchical data');
|
81
|
+
|
82
|
+
parentComponent.setState('appTitle', 'New App Title');
|
83
|
+
t.is(effectRunCount, 2, 'Binding effect should re-run after parent data change');
|
84
|
+
t.is(childComponent.testConfig, 'New App Title - Parent Child', 'Component config should update from parent data');
|
85
|
+
|
86
|
+
childComponent.setState('user.lastName', 'New Child');
|
87
|
+
t.is(effectRunCount, 3, 'Binding effect should re-run after child data change');
|
88
|
+
t.is(childComponent.testConfig, 'New App Title - Parent New Child', 'Component config should update from child data');
|
89
|
+
|
90
|
+
parentComponent.setState('user.firstName', 'New Parent');
|
91
|
+
t.is(effectRunCount, 4, 'Binding effect should re-run after parent nested data change');
|
92
|
+
t.is(childComponent.testConfig, 'New App Title - New Parent New Child', 'Component config should update from parent nested data');
|
93
|
+
|
94
|
+
parentComponent.destroy();
|
95
|
+
childComponent.destroy();
|
96
|
+
});
|
97
|
+
|
98
|
+
t.it('Provider should remove bindings on component destroy', t => {
|
99
|
+
const component = Neo.create(MockComponent, {stateProvider: {data: {test: 1}}});
|
100
|
+
const provider = component.getStateProvider();
|
101
|
+
|
102
|
+
let effectRunCount = 0;
|
103
|
+
const bindingEffect = provider.createBinding(component.id, 'testConfig', data => {
|
104
|
+
effectRunCount++;
|
105
|
+
return data.test;
|
106
|
+
});
|
107
|
+
|
108
|
+
t.is(effectRunCount, 1, 'Effect ran initially');
|
109
|
+
t.is(bindingEffect.isDestroyed, false, 'Binding effect should not be destroyed initially');
|
110
|
+
|
111
|
+
component.destroy();
|
112
|
+
t.is(bindingEffect.isDestroyed, true, 'Binding effect should be destroyed after component destroy');
|
113
|
+
|
114
|
+
provider.setData('test', 2);
|
115
|
+
t.is(effectRunCount, 1, 'Effect should not re-run after component destroyed');
|
116
|
+
});
|
117
|
+
|
118
|
+
t.it('Provider should remove bindings on provider destroy', t => {
|
119
|
+
const component = Neo.create(MockComponent, {stateProvider: {data: {test: 1}}});
|
120
|
+
const provider = component.getStateProvider();
|
121
|
+
|
122
|
+
let effectRunCount = 0;
|
123
|
+
const bindingEffect = provider.createBinding(component.id, 'testConfig', data => {
|
124
|
+
effectRunCount++;
|
125
|
+
return data.test;
|
126
|
+
});
|
127
|
+
|
128
|
+
t.is(effectRunCount, 1, 'Effect ran initially');
|
129
|
+
t.is(bindingEffect.isDestroyed, false, 'Binding effect should not be destroyed initially');
|
130
|
+
|
131
|
+
provider.destroy();
|
132
|
+
t.is(bindingEffect.isDestroyed, true, 'Binding effect should be destroyed after provider destroy');
|
133
|
+
|
134
|
+
// Attempt to change data after provider destroyed
|
135
|
+
component.setState('test', 2);
|
136
|
+
t.is(effectRunCount, 1, 'Effect should not re-run after provider destroyed');
|
137
|
+
|
138
|
+
component.destroy();
|
139
|
+
});
|
140
|
+
|
141
|
+
t.it('setData should create new data properties if they do not exist', t => {
|
142
|
+
const component = Neo.create(MockComponent, {stateProvider: {data: {}}});
|
143
|
+
const provider = component.getStateProvider();
|
144
|
+
|
145
|
+
let effectRunCount = 0;
|
146
|
+
provider.createBinding(component.id, 'testConfig', data => {
|
147
|
+
effectRunCount++;
|
148
|
+
return data.newProp;
|
149
|
+
});
|
150
|
+
|
151
|
+
t.is(effectRunCount, 1, 'Effect ran initially');
|
152
|
+
t.is(component.testConfig, undefined, 'Component config should be undefined initially');
|
153
|
+
|
154
|
+
component.setState('newProp', 'hello');
|
155
|
+
t.is(effectRunCount, 2, 'Effect re-ran after newProp was set');
|
156
|
+
t.is(component.testConfig, 'hello', 'Component config should update with newProp');
|
157
|
+
t.is(provider.getDataConfig('newProp').get(), 'hello', 'newProp config should exist');
|
158
|
+
|
159
|
+
component.setState('nested.newProp', 'world');
|
160
|
+
t.is(effectRunCount, 3, 'Effect re-ran after nested.newProp was set');
|
161
|
+
t.is(provider.getDataConfig('nested.newProp').get(), 'world', 'nested.newProp config should exist');
|
162
|
+
|
163
|
+
component.destroy();
|
164
|
+
});
|
165
|
+
|
166
|
+
t.it('Two-way binding should update state provider from component config changes', t => {
|
167
|
+
const component = Neo.create(MockComponent, {
|
168
|
+
stateProvider: {
|
169
|
+
data: {inputValue: 'initial'}
|
170
|
+
},
|
171
|
+
bind: {
|
172
|
+
testConfig: {key: 'inputValue', twoWay: true}
|
173
|
+
}
|
174
|
+
});
|
175
|
+
const provider = component.getStateProvider();
|
176
|
+
|
177
|
+
t.is(component.testConfig, 'initial', 'Component config should be initialized from state provider');
|
178
|
+
t.is(provider.getDataConfig('inputValue').get(), 'initial', 'State provider data should be initialized from component');
|
179
|
+
|
180
|
+
component.testConfig = 'updated from component';
|
181
|
+
t.is(provider.getDataConfig('inputValue').get(), 'updated from component', 'State provider data should update from component config change');
|
182
|
+
|
183
|
+
provider.setData('inputValue', 'updated from provider');
|
184
|
+
t.is(component.testConfig, 'updated from provider', 'Component config should update from state provider data change');
|
185
|
+
|
186
|
+
component.destroy();
|
187
|
+
});
|
188
|
+
|
189
|
+
t.it('Formulas should calculate correctly and react to dependencies', t => {
|
190
|
+
const component = Neo.create(MockComponent, {
|
191
|
+
stateProvider: {
|
192
|
+
data: {
|
193
|
+
price : 10,
|
194
|
+
quantity: 2
|
195
|
+
},
|
196
|
+
formulas: {
|
197
|
+
total(data) {return data.price * data.quantity},
|
198
|
+
discountedTotal: (data) => data.total * 0.9 // 10% discount
|
199
|
+
}
|
200
|
+
}
|
201
|
+
});
|
202
|
+
const provider = component.getStateProvider();
|
203
|
+
|
204
|
+
t.is(provider.getDataConfig('total').get(), 20, 'Initial total formula calculation is correct');
|
205
|
+
t.is(provider.getDataConfig('discountedTotal').get(), 18, 'Initial discountedTotal formula calculation is correct');
|
206
|
+
|
207
|
+
component.setState('price', 15);
|
208
|
+
t.is(provider.getDataConfig('total').get(), 30, 'Total formula updates when price changes');
|
209
|
+
t.is(provider.getDataConfig('discountedTotal').get(), 27, 'DiscountedTotal formula updates when total changes');
|
210
|
+
|
211
|
+
component.setState('quantity', 5);
|
212
|
+
t.is(provider.getDataConfig('total').get(), 75, 'Total formula updates when quantity changes');
|
213
|
+
t.is(provider.getDataConfig('discountedTotal').get(), 67.5, 'DiscountedTotal formula updates when total changes again');
|
214
|
+
|
215
|
+
component.destroy();
|
216
|
+
});
|
217
|
+
|
218
|
+
t.it('Store management should correctly bind components to stores and react to store changes', t => {
|
219
|
+
const store = Neo.create(Neo.data.Store, {
|
220
|
+
data : [{id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'}],
|
221
|
+
model: {fields: [{name: 'id', type: 'Int'}, {name: 'name', type: 'String'}]}
|
222
|
+
});
|
223
|
+
|
224
|
+
const component = Neo.create(MockComponent, {
|
225
|
+
stateProvider: {
|
226
|
+
stores: {
|
227
|
+
myStore: store
|
228
|
+
}
|
229
|
+
},
|
230
|
+
bind: {
|
231
|
+
testConfig: 'stores.myStore'
|
232
|
+
}
|
233
|
+
});
|
234
|
+
|
235
|
+
t.is(component.testConfig, store, 'Component config should be bound to the store instance');
|
236
|
+
t.is(component.testConfig.count, 2, 'Bound store should have correct initial count');
|
237
|
+
|
238
|
+
store.add({id: 3, name: 'Item 3'});
|
239
|
+
t.is(component.testConfig.count, 3, 'Bound store should reflect changes after adding a record');
|
240
|
+
|
241
|
+
store.remove(store.get(1));
|
242
|
+
t.is(component.testConfig.count, 2, 'Bound store should reflect changes after removing a record');
|
243
|
+
|
244
|
+
component.destroy();
|
245
|
+
store.destroy();
|
246
|
+
});
|
247
|
+
|
248
|
+
t.it('Store management with an inline store', t => {
|
249
|
+
const component = Neo.create(MockComponent, {
|
250
|
+
stateProvider: {
|
251
|
+
stores: {
|
252
|
+
myStore: {
|
253
|
+
module: Store,
|
254
|
+
data : [{id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'}],
|
255
|
+
model : {fields: [{name: 'id'}, {name: 'name'}]}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
},
|
259
|
+
bind: {
|
260
|
+
testConfig: 'stores.myStore'
|
261
|
+
}
|
262
|
+
});
|
263
|
+
|
264
|
+
const store = component.getStateProvider().getStore('myStore');
|
265
|
+
|
266
|
+
t.is(component.testConfig, store, 'Component config should be bound to the store instance');
|
267
|
+
t.is(component.testConfig.count, 2, 'Bound store should have correct initial count');
|
268
|
+
|
269
|
+
store.add({id: 3, name: 'Item 3'});
|
270
|
+
t.is(component.testConfig.count, 3, 'Bound store should reflect changes after adding a record');
|
271
|
+
|
272
|
+
store.remove(store.get(1));
|
273
|
+
t.is(component.testConfig.count, 2, 'Bound store should reflect changes after removing a record');
|
274
|
+
|
275
|
+
component.destroy();
|
276
|
+
store.destroy();
|
277
|
+
});
|
278
|
+
|
279
|
+
t.it('Provider data_ config should deep merge class and instance level data', t => {
|
280
|
+
class ClassLevelProvider extends StateProvider {
|
281
|
+
static config = {
|
282
|
+
className: 'ClassLevelProvider',
|
283
|
+
data: {
|
284
|
+
a: 1,
|
285
|
+
b: {
|
286
|
+
c: 2,
|
287
|
+
d: 3
|
288
|
+
},
|
289
|
+
arr: [1, 2]
|
290
|
+
}
|
291
|
+
}
|
292
|
+
}
|
293
|
+
Neo.setupClass(ClassLevelProvider);
|
294
|
+
|
295
|
+
// Test 1: Instance with no data, should reflect class-level data
|
296
|
+
const provider1 = Neo.create(ClassLevelProvider);
|
297
|
+
|
298
|
+
t.isDeeply(proxyToObject(provider1.data), {
|
299
|
+
a: 1,
|
300
|
+
b: {
|
301
|
+
c: 2,
|
302
|
+
d: 3
|
303
|
+
},
|
304
|
+
arr: [1, 2]
|
305
|
+
}, 'Provider1 data should reflect class-level data when no instance data is provided');
|
306
|
+
provider1.destroy();
|
307
|
+
|
308
|
+
// Test 2: Instance with new top-level data
|
309
|
+
const provider2 = Neo.create(ClassLevelProvider, {
|
310
|
+
data: {
|
311
|
+
x: 10,
|
312
|
+
y: 20
|
313
|
+
}
|
314
|
+
});
|
315
|
+
t.isDeeplyStrict(proxyToObject(provider2.data), {
|
316
|
+
a: 1,
|
317
|
+
b: {
|
318
|
+
c: 2,
|
319
|
+
d: 3
|
320
|
+
},
|
321
|
+
arr: [1, 2],
|
322
|
+
x: 10,
|
323
|
+
y: 20
|
324
|
+
}, 'Provider2 data should deep merge new top-level instance data');
|
325
|
+
provider2.destroy();
|
326
|
+
|
327
|
+
// Test 3: Instance with overlapping data (deep merge)
|
328
|
+
const provider3 = Neo.create(ClassLevelProvider, {
|
329
|
+
data: {
|
330
|
+
b: {
|
331
|
+
e: 4
|
332
|
+
},
|
333
|
+
arr: [3, 4], // Array replacement, not merge
|
334
|
+
newProp: 'test'
|
335
|
+
}
|
336
|
+
});
|
337
|
+
t.isDeeplyStrict(proxyToObject(provider3.data), {
|
338
|
+
a: 1,
|
339
|
+
b: {
|
340
|
+
c: 2,
|
341
|
+
d: 3,
|
342
|
+
e: 4
|
343
|
+
},
|
344
|
+
arr: [3, 4], // Arrays are replaced by default merge strategy
|
345
|
+
newProp: 'test'
|
346
|
+
}, 'Provider3 data should deep merge overlapping instance data and replace arrays');
|
347
|
+
provider3.destroy();
|
348
|
+
|
349
|
+
// Test 4: Instance with overlapping data (deep merge) and modifying existing nested property
|
350
|
+
const provider4 = Neo.create(ClassLevelProvider, {
|
351
|
+
data: {
|
352
|
+
b: {
|
353
|
+
c: 99
|
354
|
+
}
|
355
|
+
}
|
356
|
+
});
|
357
|
+
t.isDeeplyStrict(proxyToObject(provider4.data), {
|
358
|
+
a: 1,
|
359
|
+
b: {
|
360
|
+
c: 99,
|
361
|
+
d: 3
|
362
|
+
},
|
363
|
+
arr: [1, 2]
|
364
|
+
}, 'Provider4 data should deep merge and modify existing nested property');
|
365
|
+
provider4.destroy();
|
366
|
+
});
|
367
|
+
|
368
|
+
t.it('Provider data_ config should deep merge across multi-level class inheritance', t => {
|
369
|
+
class GrandparentProvider extends StateProvider {
|
370
|
+
static config = {
|
371
|
+
className: 'GrandparentProvider',
|
372
|
+
data: {
|
373
|
+
app: {
|
374
|
+
name: 'My App',
|
375
|
+
version: '1.0.0'
|
376
|
+
},
|
377
|
+
user: {
|
378
|
+
role: 'guest',
|
379
|
+
settings: {
|
380
|
+
theme: 'dark'
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
384
|
+
}
|
385
|
+
}
|
386
|
+
Neo.setupClass(GrandparentProvider);
|
387
|
+
|
388
|
+
class ParentProvider extends GrandparentProvider {
|
389
|
+
static config = {
|
390
|
+
className: 'ParentProvider',
|
391
|
+
data: {
|
392
|
+
app: {
|
393
|
+
version: '1.1.0', // Overrides grandparent version
|
394
|
+
author: 'Neo'
|
395
|
+
},
|
396
|
+
user: {
|
397
|
+
id: 123,
|
398
|
+
settings: {
|
399
|
+
notifications: true // Adds to grandparent settings
|
400
|
+
}
|
401
|
+
},
|
402
|
+
newParentProp: 'parentValue'
|
403
|
+
}
|
404
|
+
}
|
405
|
+
}
|
406
|
+
Neo.setupClass(ParentProvider);
|
407
|
+
|
408
|
+
class ChildProvider extends ParentProvider {
|
409
|
+
static config = {
|
410
|
+
className: 'ChildProvider',
|
411
|
+
data: {
|
412
|
+
user: {
|
413
|
+
role: 'admin', // Overrides parent role
|
414
|
+
preferences: {
|
415
|
+
language: 'en'
|
416
|
+
}
|
417
|
+
},
|
418
|
+
newChildProp: 'childValue'
|
419
|
+
}
|
420
|
+
}
|
421
|
+
}
|
422
|
+
Neo.setupClass(ChildProvider);
|
423
|
+
|
424
|
+
// Test 1: Instance with no data, should reflect merged data from all levels
|
425
|
+
const provider1 = Neo.create(ChildProvider);
|
426
|
+
t.isDeeplyStrict(proxyToObject(provider1.data), {
|
427
|
+
app: {
|
428
|
+
name: 'My App',
|
429
|
+
version: '1.1.0',
|
430
|
+
author: 'Neo'
|
431
|
+
},
|
432
|
+
user: {
|
433
|
+
role: 'admin',
|
434
|
+
id: 123,
|
435
|
+
settings: {
|
436
|
+
theme: 'dark',
|
437
|
+
notifications: true
|
438
|
+
},
|
439
|
+
preferences: {
|
440
|
+
language: 'en'
|
441
|
+
}
|
442
|
+
},
|
443
|
+
newParentProp: 'parentValue',
|
444
|
+
newChildProp: 'childValue'
|
445
|
+
}, 'Provider1 data should reflect deep merge from all class inheritance levels');
|
446
|
+
provider1.destroy();
|
447
|
+
|
448
|
+
// Test 2: Instance with data overriding properties from different levels
|
449
|
+
const provider2 = Neo.create(ChildProvider, {
|
450
|
+
data: {
|
451
|
+
app: {
|
452
|
+
version: '2.0.0', // Overrides ParentProvider's version
|
453
|
+
status: 'beta'
|
454
|
+
},
|
455
|
+
user: {
|
456
|
+
id: 456, // Overrides ParentProvider's id
|
457
|
+
settings: {
|
458
|
+
theme: 'light', // Overrides GrandparentProvider's theme
|
459
|
+
notifications: false // Overrides ParentProvider's notifications
|
460
|
+
}
|
461
|
+
},
|
462
|
+
newChildProp: 'overriddenChildValue',
|
463
|
+
instanceOnlyProp: 'instanceValue'
|
464
|
+
}
|
465
|
+
});
|
466
|
+
|
467
|
+
t.isDeeplyStrict(proxyToObject(provider2.data), {
|
468
|
+
app: {
|
469
|
+
name: 'My App',
|
470
|
+
version: '2.0.0',
|
471
|
+
author: 'Neo',
|
472
|
+
status: 'beta'
|
473
|
+
},
|
474
|
+
user: {
|
475
|
+
role: 'admin',
|
476
|
+
id: 456,
|
477
|
+
settings: {
|
478
|
+
theme: 'light',
|
479
|
+
notifications: false
|
480
|
+
},
|
481
|
+
preferences: {
|
482
|
+
language: 'en'
|
483
|
+
}
|
484
|
+
},
|
485
|
+
newParentProp: 'parentValue',
|
486
|
+
newChildProp: 'overriddenChildValue',
|
487
|
+
instanceOnlyProp: 'instanceValue'
|
488
|
+
}, 'Provider2 data should reflect deep merge with instance overrides across inheritance');
|
489
|
+
provider2.destroy();
|
490
|
+
});
|
491
|
+
|
492
|
+
t.it('Formulas in nested providers should combine own and parent data', t => {
|
493
|
+
const parentComponent = Neo.create(MockComponent, {
|
494
|
+
stateProvider: {
|
495
|
+
data: {
|
496
|
+
basePrice: 100,
|
497
|
+
taxRate : 0.05
|
498
|
+
}
|
499
|
+
}
|
500
|
+
});
|
501
|
+
|
502
|
+
const childComponent = Neo.create(MockComponent, {
|
503
|
+
parentComponent,
|
504
|
+
|
505
|
+
stateProvider: {
|
506
|
+
data: {
|
507
|
+
itemQuantity: 2
|
508
|
+
},
|
509
|
+
formulas: {
|
510
|
+
// Formula combines parent's basePrice and taxRate with child's itemQuantity
|
511
|
+
totalCost: (data) => (data.basePrice * data.itemQuantity) * (1 + data.taxRate)
|
512
|
+
}
|
513
|
+
}
|
514
|
+
});
|
515
|
+
|
516
|
+
const parentProvider = parentComponent.getStateProvider();
|
517
|
+
const childProvider = childComponent.getStateProvider();
|
518
|
+
|
519
|
+
// Initial calculation
|
520
|
+
t.is(childProvider.getData('totalCost'), (100 * 2) * (1 + 0.05), 'Initial totalCost calculation is correct');
|
521
|
+
|
522
|
+
// Change parent data
|
523
|
+
parentProvider.setData('basePrice', 120);
|
524
|
+
t.is(childProvider.getData('totalCost'), (120 * 2) * (1 + 0.05), 'totalCost updates when parent basePrice changes');
|
525
|
+
|
526
|
+
// Change child data
|
527
|
+
childProvider.setData('itemQuantity', 3);
|
528
|
+
t.is(childProvider.getData('totalCost'), (120 * 3) * (1 + 0.05), 'totalCost updates when child itemQuantity changes');
|
529
|
+
|
530
|
+
// Change parent data again
|
531
|
+
parentProvider.setData('taxRate', 0.10);
|
532
|
+
t.is(childProvider.getData('totalCost'), (120 * 3) * (1 + 0.10), 'totalCost updates when parent taxRate changes');
|
533
|
+
|
534
|
+
parentComponent.destroy();
|
535
|
+
childComponent.destroy();
|
536
|
+
});
|
537
|
+
});
|