neo.mjs 10.0.0-beta.6 → 10.0.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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
- package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +6 -4
- package/apps/portal/view/examples/List.mjs +28 -19
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/functional/button/base/MainContainer.mjs +207 -0
- package/examples/functional/button/base/app.mjs +6 -0
- package/examples/functional/button/base/index.html +11 -0
- package/examples/functional/button/base/neo-config.json +6 -0
- package/learn/blog/v10-deep-dive-functional-components.md +293 -0
- package/learn/blog/v10-deep-dive-reactivity.md +522 -0
- package/learn/blog/v10-deep-dive-state-provider.md +432 -0
- package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
- package/learn/blog/v10-post1-love-story.md +383 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +47 -45
- package/src/component/Abstract.mjs +412 -0
- package/src/component/Base.mjs +18 -380
- package/src/core/Base.mjs +34 -33
- package/src/core/Effect.mjs +30 -34
- package/src/core/EffectManager.mjs +101 -14
- package/src/core/Observable.mjs +69 -65
- package/src/form/field/Text.mjs +11 -5
- package/src/functional/button/Base.mjs +384 -0
- package/src/functional/component/Base.mjs +51 -145
- package/src/layout/Cube.mjs +8 -4
- package/src/manager/VDomUpdate.mjs +179 -94
- package/src/mixin/VdomLifecycle.mjs +4 -1
- package/src/state/Provider.mjs +41 -27
- package/src/util/VDom.mjs +11 -4
- package/src/util/vdom/TreeBuilder.mjs +38 -62
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
- package/test/siesta/siesta.js +15 -3
- package/test/siesta/tests/VdomCalendar.mjs +7 -7
- package/test/siesta/tests/VdomHelper.mjs +7 -7
- package/test/siesta/tests/classic/Button.mjs +113 -0
- package/test/siesta/tests/core/EffectBatching.mjs +46 -41
- package/test/siesta/tests/functional/Button.mjs +113 -0
- package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
- package/test/siesta/tests/vdom/Advanced.mjs +14 -8
- package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
- package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
- package/test/siesta/tests/vdom/table/Container.mjs +9 -5
- package/src/core/EffectBatchManager.mjs +0 -67
@@ -24,40 +24,43 @@ class TreeBuilder extends Base {
|
|
24
24
|
}
|
25
25
|
|
26
26
|
/**
|
27
|
-
*
|
28
|
-
* @param {Object} vdom
|
29
|
-
* @param {Number}
|
30
|
-
*
|
31
|
-
*
|
32
|
-
* @param {Set<String>|null} [mergedChildIds=null] A set of component IDs to selectively expand.
|
27
|
+
* Private helper to recursively build a tree, abstracting the child node key.
|
28
|
+
* @param {Object} node The vdom or vnode to process.
|
29
|
+
* @param {Number} depth The current recursion depth.
|
30
|
+
* @param {Set<String>|null} mergedChildIds A set of component IDs to selectively expand.
|
31
|
+
* @param {String} childKey The property name for child nodes ('cn' or 'childNodes').
|
33
32
|
* @returns {Object}
|
33
|
+
* @private
|
34
34
|
*/
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
#buildTree(node, depth, mergedChildIds, childKey) {
|
36
|
+
// We can not use Neo.isObject() here, since inside unit-test scenarios, we will import vdom.Helper into main threads.
|
37
|
+
// Inside this scenario, Neo.isObject() returns false for VNode instances
|
38
|
+
if (typeof node !== 'object' || node === null) {
|
39
|
+
return node
|
38
40
|
}
|
39
41
|
|
40
|
-
let output = {...
|
42
|
+
let output = {...node}; // Shallow copy
|
41
43
|
|
42
|
-
if (
|
43
|
-
output
|
44
|
+
if (node[childKey]) {
|
45
|
+
output[childKey] = [];
|
44
46
|
|
45
|
-
|
47
|
+
node[childKey].forEach(item => {
|
46
48
|
let currentItem = item,
|
47
49
|
childDepth;
|
48
50
|
|
49
51
|
if (currentItem.componentId) {
|
50
52
|
// Prune the branch only if we are at the boundary AND the child is not part of a merged update
|
51
53
|
if (depth === 1 && !mergedChildIds?.has(currentItem.componentId)) {
|
52
|
-
output.
|
53
|
-
// Stop processing this branch
|
54
|
-
return
|
54
|
+
output[childKey].push({componentId: 'neo-ignore', id: item.id || item.componentId});
|
55
|
+
return // Stop processing this branch
|
55
56
|
}
|
56
57
|
// Expand the branch if it's part of a merged update, or if the depth requires it
|
57
58
|
else if (depth > 1 || depth === -1 || mergedChildIds?.has(currentItem.componentId)) {
|
58
59
|
const component = ComponentManager.get(currentItem.componentId);
|
59
|
-
|
60
|
-
|
60
|
+
// Use the correct tree type based on the childKey
|
61
|
+
const componentTree = childKey === 'cn' ? component?.vdom : component?.vnode;
|
62
|
+
if (componentTree) {
|
63
|
+
currentItem = componentTree
|
61
64
|
}
|
62
65
|
}
|
63
66
|
}
|
@@ -68,61 +71,34 @@ class TreeBuilder extends Base {
|
|
68
71
|
childDepth = depth
|
69
72
|
}
|
70
73
|
|
71
|
-
output.
|
74
|
+
output[childKey].push(this.#buildTree(currentItem, childDepth, mergedChildIds, childKey))
|
72
75
|
})
|
73
76
|
}
|
74
77
|
|
75
78
|
return output
|
76
79
|
}
|
77
80
|
|
81
|
+
|
78
82
|
/**
|
79
|
-
* Copies a given
|
80
|
-
* @param {Object}
|
83
|
+
* Copies a given vdom tree and replaces child component references with their vdom.
|
84
|
+
* @param {Object} vdom
|
81
85
|
* @param {Number} [depth=-1]
|
82
|
-
*
|
83
|
-
* -1 will parse the full tree, 1 top level only, 2 include children, 3 include grandchildren
|
84
|
-
* @param {Set<String>|null} [mergedChildIds=null] A set of component IDs to selectively expand.
|
86
|
+
* @param {Set<String>|null} [mergedChildIds=null]
|
85
87
|
* @returns {Object}
|
86
88
|
*/
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
if (vnode.childNodes) {
|
91
|
-
output.childNodes = [];
|
92
|
-
|
93
|
-
vnode.childNodes.forEach(item => {
|
94
|
-
let currentItem = item,
|
95
|
-
childDepth, component;
|
96
|
-
|
97
|
-
if (currentItem.componentId) {
|
98
|
-
// Prune the branch only if we are at the boundary AND the child is not part of a merged update
|
99
|
-
if (depth === 1 && !mergedChildIds?.has(currentItem.componentId)) {
|
100
|
-
output.childNodes.push({componentId: 'neo-ignore', id: item.id || item.componentId});
|
101
|
-
// Stop processing this branch
|
102
|
-
return
|
103
|
-
}
|
104
|
-
// Expand the branch if it's part of a merged update, or if the depth requires it
|
105
|
-
else if (depth > 1 || depth === -1 || mergedChildIds?.has(currentItem.componentId)) {
|
106
|
-
component = ComponentManager.get(currentItem.componentId);
|
107
|
-
|
108
|
-
// Keep references in case there is no vnode (e.g. component not mounted yet)
|
109
|
-
if (component?.vnode) {
|
110
|
-
currentItem = component.vnode
|
111
|
-
}
|
112
|
-
}
|
113
|
-
}
|
114
|
-
|
115
|
-
if (item.componentId) {
|
116
|
-
childDepth = (depth === -1) ? -1 : Math.max(0, depth - 1)
|
117
|
-
} else {
|
118
|
-
childDepth = depth
|
119
|
-
}
|
120
|
-
|
121
|
-
output.childNodes.push(this.getVnodeTree(currentItem, childDepth, mergedChildIds))
|
122
|
-
})
|
123
|
-
}
|
89
|
+
getVdomTree(vdom, depth=-1, mergedChildIds=null) {
|
90
|
+
return this.#buildTree(vdom, depth, mergedChildIds, 'cn')
|
91
|
+
}
|
124
92
|
|
125
|
-
|
93
|
+
/**
|
94
|
+
* Copies a given vnode tree and replaces child component references with their vnode.
|
95
|
+
* @param {Object} vnode
|
96
|
+
* @param {Number} [depth=-1]
|
97
|
+
* @param {Set<String>|null} [mergedChildIds=null]
|
98
|
+
* @returns {Object}
|
99
|
+
*/
|
100
|
+
getVnodeTree(vnode, depth=-1, mergedChildIds=null) {
|
101
|
+
return this.#buildTree(vnode, depth, mergedChildIds, 'childNodes')
|
126
102
|
}
|
127
103
|
}
|
128
104
|
|
@@ -10,12 +10,7 @@ class RemoteMethodAccess extends Base {
|
|
10
10
|
* @member {String} className='Neo.worker.mixin.RemoteMethodAccess'
|
11
11
|
* @protected
|
12
12
|
*/
|
13
|
-
className: 'Neo.worker.mixin.RemoteMethodAccess'
|
14
|
-
/**
|
15
|
-
* @member {Boolean} mixin=true
|
16
|
-
* @protected
|
17
|
-
*/
|
18
|
-
mixin: true
|
13
|
+
className: 'Neo.worker.mixin.RemoteMethodAccess'
|
19
14
|
}
|
20
15
|
|
21
16
|
/**
|
package/test/siesta/siesta.js
CHANGED
@@ -70,9 +70,21 @@ project.plan(
|
|
70
70
|
'tests/vdom/table/Container.mjs'
|
71
71
|
]
|
72
72
|
},
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
'tests/vdom/Advanced.mjs',
|
74
|
+
'tests/vdom/VdomAsymmetricUpdates.mjs',
|
75
|
+
'tests/vdom/VdomRealWorldUpdates.mjs']
|
76
|
+
},
|
77
|
+
{
|
78
|
+
group: 'functional',
|
79
|
+
items: [
|
80
|
+
'tests/functional/Button.mjs'
|
81
|
+
]
|
82
|
+
},
|
83
|
+
{
|
84
|
+
group: 'classic',
|
85
|
+
items: [
|
86
|
+
'tests/classic/Button.mjs'
|
87
|
+
]
|
76
88
|
}
|
77
89
|
);
|
78
90
|
|
@@ -1,15 +1,15 @@
|
|
1
|
-
import Neo
|
2
|
-
import * as core
|
3
|
-
import NeoArray
|
4
|
-
import Style
|
5
|
-
import
|
6
|
-
import
|
1
|
+
import Neo from '../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../src/core/_export.mjs';
|
3
|
+
import NeoArray from '../../../src/util/Array.mjs';
|
4
|
+
import Style from '../../../src/util/Style.mjs';
|
5
|
+
import StringFromVnode from '../../../src/vdom/util/StringFromVnode.mjs';
|
6
|
+
import VdomHelper from '../../../src/vdom/Helper.mjs';
|
7
|
+
import VDomUtil from '../../../src/util/VDom.mjs';
|
7
8
|
|
8
9
|
let deltas, output, vdom, vnode;
|
9
10
|
|
10
11
|
// tests are designed for this rendering mode
|
11
12
|
Neo.config.useDomApiRenderer = false;
|
12
|
-
VdomHelper.onNeoConfigChange({useDomApiRenderer: false})
|
13
13
|
|
14
14
|
StartTest(t => {
|
15
15
|
t.it('Week view: Infinite Scrolling', t => {
|
@@ -1,13 +1,13 @@
|
|
1
|
-
import Neo
|
2
|
-
import * as core
|
3
|
-
import NeoArray
|
4
|
-
import Style
|
5
|
-
import
|
6
|
-
import
|
1
|
+
import Neo from '../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../src/core/_export.mjs';
|
3
|
+
import NeoArray from '../../../src/util/Array.mjs';
|
4
|
+
import Style from '../../../src/util/Style.mjs';
|
5
|
+
import StringFromVnode from '../../../src/vdom/util/StringFromVnode.mjs';
|
6
|
+
import VdomHelper from '../../../src/vdom/Helper.mjs';
|
7
|
+
import VDomUtil from '../../../src/util/VDom.mjs';
|
7
8
|
|
8
9
|
// tests are designed for this rendering mode
|
9
10
|
Neo.config.useDomApiRenderer = false;
|
10
|
-
VdomHelper.onNeoConfigChange({useDomApiRenderer: false})
|
11
11
|
|
12
12
|
let deltas, output, tmp, vdom, vnode;
|
13
13
|
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import Neo from '../../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../../src/core/_export.mjs';
|
3
|
+
import Button from '../../../../src/button/Base.mjs';
|
4
|
+
import DomApiVnodeCreator from '../../../../src/vdom/util/DomApiVnodeCreator.mjs';
|
5
|
+
import VdomHelper from '../../../../src/vdom/Helper.mjs';
|
6
|
+
|
7
|
+
// IMPORTANT: This test file uses real components and expects them to render.
|
8
|
+
// We need to enable unitTestMode for isolation, but also allow VDOM updates.
|
9
|
+
Neo.config.unitTestMode = true;
|
10
|
+
Neo.config.allowVdomUpdatesInTests = true;
|
11
|
+
// This ensures that the VdomHelper uses the correct renderer for the assertions.
|
12
|
+
Neo.config.useDomApiRenderer = true;
|
13
|
+
|
14
|
+
// Create a mock application context, as the component lifecycle requires it for updates.
|
15
|
+
const appName = 'ClassicButtonTest';
|
16
|
+
Neo.apps = Neo.apps || {};
|
17
|
+
Neo.apps[appName] = {
|
18
|
+
name : appName,
|
19
|
+
fire : Neo.emptyFn,
|
20
|
+
isMounted: () => true,
|
21
|
+
rendering: false
|
22
|
+
};
|
23
|
+
|
24
|
+
StartTest(t => {
|
25
|
+
let button, vnode;
|
26
|
+
let testRun = 0;
|
27
|
+
|
28
|
+
t.beforeEach(async t => {
|
29
|
+
testRun++;
|
30
|
+
|
31
|
+
button = Neo.create(Button, {
|
32
|
+
appName,
|
33
|
+
id : 'my-button-' + testRun,
|
34
|
+
iconCls: 'fa fa-home',
|
35
|
+
text : 'Click me'
|
36
|
+
});
|
37
|
+
|
38
|
+
({vnode} = await button.render());
|
39
|
+
button.mounted = true; // Manually mount to enable updates in the test env
|
40
|
+
});
|
41
|
+
|
42
|
+
t.afterEach(t => {
|
43
|
+
button?.destroy();
|
44
|
+
button = null;
|
45
|
+
vnode = null;
|
46
|
+
});
|
47
|
+
|
48
|
+
t.it('should create initial vnode correctly', async t => {
|
49
|
+
t.expect(vnode.nodeName).toBe('button');
|
50
|
+
t.expect(vnode.className).toEqual(['neo-button', 'icon-left']);
|
51
|
+
t.expect(vnode.childNodes.length).toBe(2); // icon & text. badge & ripple have removeDom:true
|
52
|
+
|
53
|
+
const iconNode = vnode.childNodes[0];
|
54
|
+
t.expect(iconNode.className).toEqual(['neo-button-glyph', 'fa', 'fa-home']);
|
55
|
+
|
56
|
+
const textNode = vnode.childNodes[1];
|
57
|
+
t.expect(textNode.className).toEqual(['neo-button-text']);
|
58
|
+
t.expect(textNode.textContent).toBe('Click me');
|
59
|
+
});
|
60
|
+
|
61
|
+
t.it('should update vnode and create delta for a single config change', async t => {
|
62
|
+
const textNodeId = vnode.childNodes[1].id;
|
63
|
+
const {deltas} = await button.set({text: 'New Text'});
|
64
|
+
|
65
|
+
t.is(deltas.length, 1, 'Should generate exactly one delta');
|
66
|
+
const delta = deltas[0];
|
67
|
+
|
68
|
+
t.is(delta.id, textNodeId, 'Delta should target the text node');
|
69
|
+
t.is(delta.textContent, 'New Text', 'Delta textContent is correct');
|
70
|
+
});
|
71
|
+
|
72
|
+
t.it('should update vnode and create delta for multiple config changes', async t => {
|
73
|
+
const iconNodeId = vnode.childNodes[0].id;
|
74
|
+
const textNodeId = vnode.childNodes[1].id;
|
75
|
+
|
76
|
+
const {deltas} = await button.set({
|
77
|
+
iconCls: 'fa fa-user',
|
78
|
+
text : 'Submit'
|
79
|
+
});
|
80
|
+
|
81
|
+
t.is(deltas.length, 2, 'Should generate exactly two deltas');
|
82
|
+
|
83
|
+
const iconDelta = deltas.find(d => d.id === iconNodeId);
|
84
|
+
const textDelta = deltas.find(d => d.id === textNodeId);
|
85
|
+
|
86
|
+
t.ok(iconDelta, 'Should have a delta for the icon node');
|
87
|
+
t.isDeeply(iconDelta.cls.remove, ['fa-home'], 'Icon delta should remove old class');
|
88
|
+
t.isDeeply(iconDelta.cls.add, ['fa-user'], 'Icon delta should add new class');
|
89
|
+
|
90
|
+
t.ok(textDelta, 'Should have a delta for the text node');
|
91
|
+
t.is(textDelta.textContent, 'Submit', 'Text delta is correct');
|
92
|
+
});
|
93
|
+
|
94
|
+
t.it('should handle pressed state change', async t => {
|
95
|
+
t.notOk(vnode.className.includes('pressed'), 'Initial vnode should not have "pressed" class');
|
96
|
+
|
97
|
+
let updateData = await button.set({pressed: true});
|
98
|
+
|
99
|
+
t.is(updateData.deltas.length, 1, 'Should generate one delta for pressed: true');
|
100
|
+
let delta = updateData.deltas[0];
|
101
|
+
t.is(delta.id, button.id, 'Delta should target the button component root');
|
102
|
+
t.isDeeply(delta.cls.add, ['pressed'], 'Delta should add "pressed" class');
|
103
|
+
t.ok(updateData.vnode.className.includes('pressed'), 'Vnode should have "pressed" class');
|
104
|
+
|
105
|
+
updateData = await button.set({pressed: false});
|
106
|
+
|
107
|
+
t.is(updateData.deltas.length, 1, 'Should generate one delta for pressed: false');
|
108
|
+
delta = updateData.deltas[0];
|
109
|
+
t.is(delta.id, button.id, 'Delta should target the button component root');
|
110
|
+
t.isDeeply(delta.cls.remove, ['pressed'], 'Delta should remove "pressed" class');
|
111
|
+
t.notOk(updateData.vnode.className.includes('pressed'), 'Vnode should not have "pressed" class');
|
112
|
+
});
|
113
|
+
});
|
@@ -1,23 +1,24 @@
|
|
1
|
-
import Neo
|
2
|
-
import * as core
|
3
|
-
import Effect
|
4
|
-
import
|
1
|
+
import Neo from '../../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../../src/core/_export.mjs';
|
3
|
+
import Effect from '../../../../src/core/Effect.mjs';
|
4
|
+
import EffectManager from '../../../../src/core/EffectManager.mjs';
|
5
5
|
|
6
6
|
class TestComponent extends core.Base {
|
7
7
|
static config = {
|
8
8
|
className: 'Neo.Test.EffectBatchingComponent',
|
9
|
-
configA_: 0,
|
10
|
-
configB_: 0,
|
11
|
-
configC_: 0
|
9
|
+
configA_ : 0,
|
10
|
+
configB_ : 0,
|
11
|
+
configC_ : 0
|
12
12
|
}
|
13
13
|
}
|
14
|
+
|
14
15
|
Neo.setupClass(TestComponent);
|
15
16
|
|
16
17
|
StartTest(t => {
|
17
18
|
t.it('Effects should be batched during core.Base#set() operations', t => {
|
18
|
-
const instance
|
19
|
+
const instance = Neo.create(TestComponent);
|
19
20
|
let effectRunCount = 0;
|
20
|
-
let sum
|
21
|
+
let sum = 0;
|
21
22
|
|
22
23
|
const effect = new Effect(() => {
|
23
24
|
effectRunCount++;
|
@@ -47,7 +48,7 @@ StartTest(t => {
|
|
47
48
|
t.is(instance.configC, 3, 'configC should be updated');
|
48
49
|
|
49
50
|
// Test individual property change outside a batch
|
50
|
-
effectRunCount
|
51
|
+
effectRunCount = 0;
|
51
52
|
instance.configA = 10;
|
52
53
|
t.is(effectRunCount, 1, 'Effect should run immediately for individual property change outside a batch');
|
53
54
|
t.is(sum, 10 + 2 + 3, 'Sum should be 10 + 2 + 3 = 15 after individual configA change');
|
@@ -58,8 +59,8 @@ StartTest(t => {
|
|
58
59
|
// Update all configs to their previous values (no change)
|
59
60
|
instance.set({
|
60
61
|
configA: 10,
|
61
|
-
configB:
|
62
|
-
configC:
|
62
|
+
configB: 2,
|
63
|
+
configC: 3
|
63
64
|
});
|
64
65
|
|
65
66
|
t.is(effectRunCount, 0, 'Effect should not run.');
|
@@ -68,28 +69,28 @@ StartTest(t => {
|
|
68
69
|
instance.destroy();
|
69
70
|
});
|
70
71
|
|
71
|
-
t.it('
|
72
|
-
t.is(
|
72
|
+
t.it('EffectManager should correctly manage batch state via pause() and resume()', t => {
|
73
|
+
t.is(EffectManager.isPaused(), false, 'Batch should not be active initially');
|
73
74
|
|
74
|
-
|
75
|
-
t.is(
|
75
|
+
EffectManager.pause();
|
76
|
+
t.is(EffectManager.isPaused(), true, 'Batch should be active after pause()');
|
76
77
|
|
77
|
-
|
78
|
-
t.is(
|
78
|
+
EffectManager.pause(); // Nested batch
|
79
|
+
t.is(EffectManager.isPaused(), true, 'Batch should still be active for nested pause()');
|
79
80
|
|
80
|
-
|
81
|
-
t.is(
|
81
|
+
EffectManager.resume();
|
82
|
+
t.is(EffectManager.isPaused(), true, 'Batch should still be active after inner resume()');
|
82
83
|
|
83
|
-
|
84
|
-
t.is(
|
84
|
+
EffectManager.resume();
|
85
|
+
t.is(EffectManager.isPaused(), false, 'Batch should not be active after all resume() calls');
|
85
86
|
});
|
86
87
|
|
87
88
|
t.it('Effect should run when a dependency is changed inside an afterSet hook', t => {
|
88
89
|
class IndirectDependencyComponent extends core.Base {
|
89
90
|
static config = {
|
90
91
|
className: 'Neo.Test.IndirectDependencyComponent',
|
91
|
-
configA_: 'initialA',
|
92
|
-
configB_: 'initialB'
|
92
|
+
configA_ : 'initialA',
|
93
|
+
configB_ : 'initialB'
|
93
94
|
}
|
94
95
|
|
95
96
|
afterSetConfigA(newValue, oldValue) {
|
@@ -99,11 +100,12 @@ StartTest(t => {
|
|
99
100
|
}
|
100
101
|
}
|
101
102
|
}
|
103
|
+
|
102
104
|
Neo.setupClass(IndirectDependencyComponent);
|
103
105
|
|
104
|
-
const instance
|
106
|
+
const instance = Neo.create(IndirectDependencyComponent);
|
105
107
|
let effectRunCount = 0;
|
106
|
-
let effectValue
|
108
|
+
let effectValue = '';
|
107
109
|
|
108
110
|
// Effect depends only on configB
|
109
111
|
const effect = new Effect(() => {
|
@@ -135,8 +137,8 @@ StartTest(t => {
|
|
135
137
|
class BeforeSetDependencyComponent extends core.Base {
|
136
138
|
static config = {
|
137
139
|
className: 'Neo.Test.BeforeSetDependencyComponent',
|
138
|
-
configA_: 'initialA',
|
139
|
-
configC_: 'initialC' // configC will be changed by beforeSetConfigA
|
140
|
+
configA_ : 'initialA',
|
141
|
+
configC_ : 'initialC' // configC will be changed by beforeSetConfigA
|
140
142
|
}
|
141
143
|
|
142
144
|
beforeSetConfigA(newValue, oldValue) {
|
@@ -147,11 +149,12 @@ StartTest(t => {
|
|
147
149
|
return newValue; // Important: return newValue to allow configA to be set
|
148
150
|
}
|
149
151
|
}
|
152
|
+
|
150
153
|
Neo.setupClass(BeforeSetDependencyComponent);
|
151
154
|
|
152
|
-
const instance
|
155
|
+
const instance = Neo.create(BeforeSetDependencyComponent);
|
153
156
|
let effectRunCount = 0;
|
154
|
-
let effectValue
|
157
|
+
let effectValue = '';
|
155
158
|
|
156
159
|
// Effect depends only on configC
|
157
160
|
const effect = new Effect(() => {
|
@@ -183,9 +186,9 @@ StartTest(t => {
|
|
183
186
|
class CombinedDependencyComponent extends core.Base {
|
184
187
|
static config = {
|
185
188
|
className: 'Neo.Test.CombinedDependencyComponent',
|
186
|
-
configA_: 'initialA',
|
187
|
-
configB_: 'initialB', // Changed by afterSet
|
188
|
-
configC_: 'initialC' // Changed by beforeSet
|
189
|
+
configA_ : 'initialA',
|
190
|
+
configB_ : 'initialB', // Changed by afterSet
|
191
|
+
configC_ : 'initialC' // Changed by beforeSet
|
189
192
|
}
|
190
193
|
|
191
194
|
beforeSetConfigA(newValue, oldValue) {
|
@@ -201,13 +204,14 @@ StartTest(t => {
|
|
201
204
|
}
|
202
205
|
}
|
203
206
|
}
|
207
|
+
|
204
208
|
Neo.setupClass(CombinedDependencyComponent);
|
205
209
|
|
206
|
-
const instance
|
210
|
+
const instance = Neo.create(CombinedDependencyComponent);
|
207
211
|
let effectBRunCount = 0;
|
208
212
|
let effectCRunCount = 0;
|
209
|
-
let effectBValue
|
210
|
-
let effectCValue
|
213
|
+
let effectBValue = '';
|
214
|
+
let effectCValue = '';
|
211
215
|
|
212
216
|
// Effect for configB (changed by afterSet)
|
213
217
|
const effectB = new Effect(() => {
|
@@ -257,9 +261,9 @@ StartTest(t => {
|
|
257
261
|
class SingleEffectCombinedDependencyComponent extends core.Base {
|
258
262
|
static config = {
|
259
263
|
className: 'Neo.Test.SingleEffectCombinedDependencyComponent',
|
260
|
-
configA_: 'initialA',
|
261
|
-
configB_: 'initialB', // Changed by afterSet
|
262
|
-
configC_: 'initialC' // Changed by beforeSet
|
264
|
+
configA_ : 'initialA',
|
265
|
+
configB_ : 'initialB', // Changed by afterSet
|
266
|
+
configC_ : 'initialC' // Changed by beforeSet
|
263
267
|
}
|
264
268
|
|
265
269
|
beforeSetConfigA(newValue, oldValue) {
|
@@ -275,11 +279,12 @@ StartTest(t => {
|
|
275
279
|
}
|
276
280
|
}
|
277
281
|
}
|
282
|
+
|
278
283
|
Neo.setupClass(SingleEffectCombinedDependencyComponent);
|
279
284
|
|
280
|
-
const instance
|
285
|
+
const instance = Neo.create(SingleEffectCombinedDependencyComponent);
|
281
286
|
let effectRunCount = 0;
|
282
|
-
let effectValue
|
287
|
+
let effectValue = '';
|
283
288
|
|
284
289
|
// Single Effect depends on both configB and configC
|
285
290
|
const effect = new Effect(() => {
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import Neo from '../../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../../src/core/_export.mjs';
|
3
|
+
import Button from '../../../../src/functional/button/Base.mjs';
|
4
|
+
import DomApiVnodeCreator from '../../../../src/vdom/util/DomApiVnodeCreator.mjs';
|
5
|
+
import VdomHelper from '../../../../src/vdom/Helper.mjs';
|
6
|
+
|
7
|
+
// IMPORTANT: This test file uses real components and expects them to render.
|
8
|
+
// We need to enable unitTestMode for isolation, but also allow VDOM updates.
|
9
|
+
Neo.config.unitTestMode = true;
|
10
|
+
Neo.config.allowVdomUpdatesInTests = true;
|
11
|
+
// This ensures that the VdomHelper uses the correct renderer for the assertions.
|
12
|
+
Neo.config.useDomApiRenderer = true;
|
13
|
+
|
14
|
+
// Create a mock application context, as the component lifecycle requires it for updates.
|
15
|
+
const appName = 'FunctionalButtonTest';
|
16
|
+
Neo.apps = Neo.apps || {};
|
17
|
+
Neo.apps[appName] = {
|
18
|
+
name : appName,
|
19
|
+
fire : Neo.emptyFn,
|
20
|
+
isMounted: () => true,
|
21
|
+
rendering: false
|
22
|
+
};
|
23
|
+
|
24
|
+
StartTest(t => {
|
25
|
+
let button, vnode;
|
26
|
+
let testRun = 0;
|
27
|
+
|
28
|
+
t.beforeEach(async t => {
|
29
|
+
testRun++;
|
30
|
+
|
31
|
+
button = Neo.create(Button, {
|
32
|
+
appName,
|
33
|
+
id : 'my-button-' + testRun,
|
34
|
+
iconCls: 'fa fa-home',
|
35
|
+
text : 'Click me'
|
36
|
+
});
|
37
|
+
|
38
|
+
({vnode} = await button.render());
|
39
|
+
button.mounted = true; // Manually mount to enable updates in the test env
|
40
|
+
});
|
41
|
+
|
42
|
+
t.afterEach(t => {
|
43
|
+
button?.destroy();
|
44
|
+
button = null;
|
45
|
+
vnode = null;
|
46
|
+
});
|
47
|
+
|
48
|
+
t.it('should create initial vnode correctly', async t => {
|
49
|
+
t.expect(vnode.nodeName).toBe('button');
|
50
|
+
t.expect(vnode.className).toEqual(['neo-button', 'icon-left']);
|
51
|
+
t.expect(vnode.childNodes.length).toBe(2); // icon & text. badge & ripple have removeDom:true
|
52
|
+
|
53
|
+
const iconNode = vnode.childNodes[0];
|
54
|
+
t.expect(iconNode.className).toEqual(['neo-button-glyph', 'fa', 'fa-home']);
|
55
|
+
|
56
|
+
const textNode = vnode.childNodes[1];
|
57
|
+
t.expect(textNode.className).toEqual(['neo-button-text']);
|
58
|
+
t.expect(textNode.textContent).toBe('Click me');
|
59
|
+
});
|
60
|
+
|
61
|
+
t.it('should update vnode and create delta for a single config change', async t => {
|
62
|
+
const textNodeId = vnode.childNodes[1].id;
|
63
|
+
const {deltas} = await button.set({text: 'New Text'});
|
64
|
+
|
65
|
+
t.is(deltas.length, 1, 'Should generate exactly one delta');
|
66
|
+
const delta = deltas[0];
|
67
|
+
|
68
|
+
t.is(delta.id, textNodeId, 'Delta should target the text node');
|
69
|
+
t.is(delta.textContent, 'New Text', 'Delta textContent is correct');
|
70
|
+
});
|
71
|
+
|
72
|
+
t.it('should update vnode and create delta for multiple config changes', async t => {
|
73
|
+
const iconNodeId = vnode.childNodes[0].id;
|
74
|
+
const textNodeId = vnode.childNodes[1].id;
|
75
|
+
|
76
|
+
const {deltas} = await button.set({
|
77
|
+
iconCls: 'fa fa-user',
|
78
|
+
text : 'Submit'
|
79
|
+
});
|
80
|
+
|
81
|
+
t.is(deltas.length, 2, 'Should generate exactly two deltas');
|
82
|
+
|
83
|
+
const iconDelta = deltas.find(d => d.id === iconNodeId);
|
84
|
+
const textDelta = deltas.find(d => d.id === textNodeId);
|
85
|
+
|
86
|
+
t.ok(iconDelta, 'Should have a delta for the icon node');
|
87
|
+
t.isDeeply(iconDelta.cls.remove, ['fa-home'], 'Icon delta should remove old class');
|
88
|
+
t.isDeeply(iconDelta.cls.add, ['fa-user'], 'Icon delta should add new class');
|
89
|
+
|
90
|
+
t.ok(textDelta, 'Should have a delta for the text node');
|
91
|
+
t.is(textDelta.textContent, 'Submit', 'Text delta is correct');
|
92
|
+
});
|
93
|
+
|
94
|
+
t.it('should handle pressed state change', async t => {
|
95
|
+
t.notOk(vnode.className.includes('pressed'), 'Initial vnode should not have "pressed" class');
|
96
|
+
|
97
|
+
let updateData = await button.set({pressed: true});
|
98
|
+
|
99
|
+
t.is(updateData.deltas.length, 1, 'Should generate one delta for pressed: true');
|
100
|
+
let delta = updateData.deltas[0];
|
101
|
+
t.is(delta.id, button.id, 'Delta should target the button component root');
|
102
|
+
t.isDeeply(delta.cls.add, ['pressed'], 'Delta should add "pressed" class');
|
103
|
+
t.ok(updateData.vnode.className.includes('pressed'), 'Vnode should have "pressed" class');
|
104
|
+
|
105
|
+
updateData = await button.set({pressed: false});
|
106
|
+
|
107
|
+
t.is(updateData.deltas.length, 1, 'Should generate one delta for pressed: false');
|
108
|
+
delta = updateData.deltas[0];
|
109
|
+
t.is(delta.id, button.id, 'Delta should target the button component root');
|
110
|
+
t.isDeeply(delta.cls.remove, ['pressed'], 'Delta should remove "pressed" class');
|
111
|
+
t.notOk(updateData.vnode.className.includes('pressed'), 'Vnode should not have "pressed" class');
|
112
|
+
});
|
113
|
+
});
|