|
| 1 | +import Neo from '../../../../src/Neo.mjs'; |
| 2 | +import * as core from '../../../../src/core/_export.mjs'; |
| 3 | +import ComponentManager from '../../../../src/manager/Component.mjs'; |
| 4 | +import TreeBuilder from '../../../../src/util/vdom/TreeBuilder.mjs'; |
| 5 | +import VDomUpdate from '../../../../src/manager/VDomUpdate.mjs'; |
| 6 | +import VdomHelper from '../../../../src/vdom/Helper.mjs'; |
| 7 | +import VDomUtil from '../../../../src/util/VDom.mjs'; |
| 8 | + |
| 9 | +// IMPORTANT: Test with the new standard renderer |
| 10 | +Neo.config.useDomApiRenderer = true; |
| 11 | +VdomHelper.onNeoConfigChange({useDomApiRenderer: true}); |
| 12 | + |
| 13 | +/** |
| 14 | + * Creates a mock component object for testing. |
| 15 | + * @param {string} id |
| 16 | + * @param {string} parentId |
| 17 | + * @param {Object} vdom |
| 18 | + * @returns {Object} A mock component |
| 19 | + */ |
| 20 | +const createMockComponent = (id, parentId, vdom) => { |
| 21 | + const component = { |
| 22 | + id, |
| 23 | + parentId, |
| 24 | + vdom |
| 25 | + }; |
| 26 | + // Create the initial vnode from the vdom definition. |
| 27 | + const { vnode } = VdomHelper.create({ vdom }); |
| 28 | + component.vnode = vnode; |
| 29 | + |
| 30 | + // Register the component BEFORE syncing IDs. This is critical so that |
| 31 | + // a parent's syncVdomIds call can find this component if it's a child. |
| 32 | + ComponentManager.register(component); |
| 33 | + VDomUtil.syncVdomIds(component.vnode, component.vdom); |
| 34 | + |
| 35 | + return component; |
| 36 | +}; |
| 37 | + |
| 38 | +StartTest(t => { |
| 39 | + |
| 40 | + t.beforeEach(() => { |
| 41 | + // Reset managers to ensure test isolation |
| 42 | + VDomUpdate.mergedCallbackMap.clear(); |
| 43 | + VDomUpdate.postUpdateQueueMap.clear(); |
| 44 | + ComponentManager.wrapperNodes.clear(); |
| 45 | + ComponentManager.clear(); |
| 46 | + }); |
| 47 | + |
| 48 | + t.it('Should handle asymmetric update with depth 2 using DomApiRenderer', t => { |
| 49 | + // 1. SETUP |
| 50 | + // Create a parent and a child. The parent's vdom references the child via componentId. |
| 51 | + const childVdomInitial = { id: 'child-1', cn: [{ tag: 'span', text: 'Initial' }] }; |
| 52 | + const parentVdom = { |
| 53 | + id: 'parent-1', |
| 54 | + cn: [{ componentId: 'child-1' }] |
| 55 | + }; |
| 56 | + |
| 57 | + // Create components dependency-first (child before parent) to ensure |
| 58 | + // component references can be resolved during VDOM/VNode processing. |
| 59 | + // The `createMockComponent` factory now handles registration. |
| 60 | + let child = createMockComponent('child-1', 'parent-1', childVdomInitial); |
| 61 | + let parent = createMockComponent('parent-1', 'root', parentVdom); |
| 62 | + |
| 63 | + // 2. SIMULATE A CHILD-INITIATED UPDATE |
| 64 | + // The child's internal state changes, and it requests to be part of the parent's next update. |
| 65 | + VDomUpdate.registerMerged( |
| 66 | + parent.id, |
| 67 | + child.id, |
| 68 | + [], // callbacks |
| 69 | + 1, // childUpdateDepth |
| 70 | + 1 // distance |
| 71 | + ); |
| 72 | + |
| 73 | + // The child's vdom has now changed. We update our mock to reflect this. |
| 74 | + const childVdomUpdated = { id: 'child-1', cn: [{ tag: 'span', text: 'Updated' }] }; |
| 75 | + child.vdom = childVdomUpdated; |
| 76 | + |
| 77 | + // 3. SIMULATE THE PARENT'S UPDATE LIFECYCLE |
| 78 | + // The parent calculates the required depth for the update. |
| 79 | + const adjustedDepth = VDomUpdate.getAdjustedUpdateDepth(parent.id); |
| 80 | + t.is(adjustedDepth, 2, 'Adjusted update depth should be 2 to include direct children'); |
| 81 | + |
| 82 | + // The parent builds an asymmetric VDOM tree. TreeBuilder will find the updated |
| 83 | + // child.vdom via the ComponentManager. |
| 84 | + const newAsymmetricVdom = TreeBuilder.getVdomTree(parent.vdom, adjustedDepth); |
| 85 | + |
| 86 | + // Verify the created tree has the child's *new* vdom |
| 87 | + t.is(newAsymmetricVdom.cn[0].id, 'child-1', 'The child component VDOM is expanded in the asymmetric tree'); |
| 88 | + t.is(newAsymmetricVdom.cn[0].cn[0].text, 'Updated', 'The expanded VDOM reflects the childs updated state'); |
| 89 | + |
| 90 | + // 4. GENERATE DELTAS |
| 91 | + // VdomHelper diffs the new, expanded tree against the parent's OLD vnode. |
| 92 | + const { deltas } = VdomHelper.update({ |
| 93 | + vdom : newAsymmetricVdom, |
| 94 | + vnode: parent.vnode |
| 95 | + }); |
| 96 | + |
| 97 | + // 5. ASSERTIONS |
| 98 | + t.is(deltas.length, 1, 'Should generate exactly one delta for the text change'); |
| 99 | + const delta = deltas[0]; |
| 100 | + t.is(delta.action, 'updateVtext', 'The delta action should be to update the text node'); |
| 101 | + t.is(delta.value, 'Updated', 'The new text content should be correct'); |
| 102 | + t.ok(delta.id.startsWith('neo-vtext'), 'The delta correctly targets the text vnode'); |
| 103 | + }); |
| 104 | + |
| 105 | + t.it('Should handle nested asymmetric update (grandchild update)', t => { |
| 106 | + // 1. SETUP |
| 107 | + const grandchildVdomInitial = { id: 'grandchild-1', cn: [{ tag: 'span', text: 'Initial' }] }; |
| 108 | + const childVdom = { |
| 109 | + id: 'child-1', |
| 110 | + cn: [{ componentId: 'grandchild-1' }] |
| 111 | + }; |
| 112 | + const parentVdom = { |
| 113 | + id: 'parent-1', |
| 114 | + cn: [{ componentId: 'child-1' }] |
| 115 | + }; |
| 116 | + |
| 117 | + // Create components dependency-first (grandchild -> child -> parent) to ensure |
| 118 | + // component references can be resolved during VDOM/VNode processing. |
| 119 | + let grandchild = createMockComponent('grandchild-1', 'child-1', grandchildVdomInitial); |
| 120 | + let child = createMockComponent('child-1', 'parent-1', childVdom); |
| 121 | + let parent = createMockComponent('parent-1', 'root', parentVdom); |
| 122 | + |
| 123 | + // 2. SIMULATE A GRANDCHILD-INITIATED UPDATE |
| 124 | + // The grandchild's state changes. It is at a distance of 2 from the updating parent. |
| 125 | + VDomUpdate.registerMerged( |
| 126 | + parent.id, |
| 127 | + grandchild.id, |
| 128 | + [], // callbacks |
| 129 | + 1, // grandchild's own updateDepth |
| 130 | + 2 // distance from parent |
| 131 | + ); |
| 132 | + |
| 133 | + // The grandchild's vdom has now changed. |
| 134 | + const grandchildVdomUpdated = { id: 'grandchild-1', cn: [{ tag: 'span', text: 'Updated' }] }; |
| 135 | + grandchild.vdom = grandchildVdomUpdated; |
| 136 | + |
| 137 | + // 3. SIMULATE THE PARENT'S UPDATE LIFECYCLE |
| 138 | + // The required depth for the parent should be 3 to expand down to the grandchild. |
| 139 | + const adjustedDepth = VDomUpdate.getAdjustedUpdateDepth(parent.id); |
| 140 | + t.is(adjustedDepth, 3, 'Adjusted update depth should be 3 to include grandchild'); |
| 141 | + |
| 142 | + // The parent builds an asymmetric VDOM tree. |
| 143 | + const newAsymmetricVdom = TreeBuilder.getVdomTree(parent.vdom, adjustedDepth); |
| 144 | + |
| 145 | + // Verify the created tree has the grandchild's *new* vdom |
| 146 | + const expandedChild = newAsymmetricVdom.cn[0]; |
| 147 | + const expandedGrandchild = expandedChild.cn[0]; |
| 148 | + t.is(expandedGrandchild.id, 'grandchild-1', 'The grandchild component VDOM is expanded in the asymmetric tree'); |
| 149 | + t.is(expandedGrandchild.cn[0].text, 'Updated', 'The expanded VDOM reflects the grandchilds updated state'); |
| 150 | + |
| 151 | + // 4. GENERATE DELTAS |
| 152 | + const { deltas } = VdomHelper.update({ |
| 153 | + vdom : newAsymmetricVdom, |
| 154 | + vnode: parent.vnode |
| 155 | + }); |
| 156 | + |
| 157 | + // 5. ASSERTIONS |
| 158 | + t.is(deltas.length, 1, 'Should generate exactly one delta for the text change'); |
| 159 | + t.is(deltas[0].action, 'updateVtext', 'The delta action should be to update the text node'); |
| 160 | + }); |
| 161 | +}); |
0 commit comments