|
1 | | -import {once} from '@spearwolf/eventize'; |
| 1 | +import {on, once} from '@spearwolf/eventize'; |
2 | 2 | import {afterEach, describe, expect, it, vi} from 'vitest'; |
3 | 3 | import {Registry} from '../in-the-dark/Registry.js'; |
4 | 4 | import {ShadowObject} from '../in-the-dark/ShadowObject.js'; |
| 5 | +import type {ShadowObjectParams} from '../types.js'; |
5 | 6 | import {ComponentContext} from './ComponentContext.js'; |
6 | 7 | import {LocalShadowObjectEnv} from './LocalShadowObjectEnv.js'; |
7 | 8 | import {ShadowEnv} from './ShadowEnv.js'; |
@@ -87,4 +88,115 @@ describe('ShadowEnv', () => { |
87 | 88 |
|
88 | 89 | env.destroy(); |
89 | 90 | }); |
| 91 | + |
| 92 | + it('should dispatch MessageToView with traverseChildren=true through the entire stack', async () => { |
| 93 | + const env = new ShadowEnv(); |
| 94 | + env.view = ComponentContext.get(); |
| 95 | + env.envProxy = new LocalShadowObjectEnv(); |
| 96 | + |
| 97 | + // Create a hierarchy of view components |
| 98 | + const parentVC = new ViewComponent('parent', {context: env.view}); |
| 99 | + const childVC1 = new ViewComponent('child1', {context: env.view, parent: parentVC}); |
| 100 | + const childVC2 = new ViewComponent('child2', {context: env.view, parent: parentVC}); |
| 101 | + const grandChildVC = new ViewComponent('grandChild', {context: env.view, parent: childVC1}); |
| 102 | + |
| 103 | + // Set up spies on ViewComponents to track event dispatch |
| 104 | + const parentSpy = vi.fn(); |
| 105 | + const child1Spy = vi.fn(); |
| 106 | + const child2Spy = vi.fn(); |
| 107 | + const grandChildSpy = vi.fn(); |
| 108 | + |
| 109 | + on(parentVC, 'myEvent', parentSpy); |
| 110 | + on(childVC1, 'myEvent', child1Spy); |
| 111 | + on(childVC2, 'myEvent', child2Spy); |
| 112 | + on(grandChildVC, 'myEvent', grandChildSpy); |
| 113 | + |
| 114 | + // Define a shadow object that will dispatch the message with traverseChildren |
| 115 | + let entityRef: { |
| 116 | + dispatchMessageToView: (type: string, data?: unknown, transferables?: Transferable[], traverseChildren?: boolean) => void; |
| 117 | + } | null = null; |
| 118 | + |
| 119 | + @ShadowObject({token: 'parent'}) |
| 120 | + class ParentShadowObject { |
| 121 | + constructor({entity}: ShadowObjectParams) { |
| 122 | + entityRef = entity; |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + expect(ParentShadowObject).toBeDefined(); |
| 127 | + |
| 128 | + await env.syncWait(); |
| 129 | + |
| 130 | + // Verify shadow object was created and we have the entity reference |
| 131 | + expect(entityRef).not.toBeNull(); |
| 132 | + |
| 133 | + // Dispatch a message with traverseChildren=true from the shadow object |
| 134 | + entityRef!.dispatchMessageToView('myEvent', {testData: 'hello'}, undefined, true); |
| 135 | + |
| 136 | + // Wait for the microtask queue to process |
| 137 | + await new Promise((resolve) => setTimeout(resolve, 50)); |
| 138 | + |
| 139 | + // Verify the event was dispatched to parent and all descendants |
| 140 | + expect(parentSpy).toHaveBeenCalledTimes(1); |
| 141 | + expect(parentSpy).toHaveBeenCalledWith({testData: 'hello'}); |
| 142 | + |
| 143 | + expect(child1Spy).toHaveBeenCalledTimes(1); |
| 144 | + expect(child1Spy).toHaveBeenCalledWith({testData: 'hello'}); |
| 145 | + |
| 146 | + expect(child2Spy).toHaveBeenCalledTimes(1); |
| 147 | + expect(child2Spy).toHaveBeenCalledWith({testData: 'hello'}); |
| 148 | + |
| 149 | + expect(grandChildSpy).toHaveBeenCalledTimes(1); |
| 150 | + expect(grandChildSpy).toHaveBeenCalledWith({testData: 'hello'}); |
| 151 | + |
| 152 | + env.destroy(); |
| 153 | + }); |
| 154 | + |
| 155 | + it('should dispatch MessageToView with traverseChildren=false only to the target component', async () => { |
| 156 | + const env = new ShadowEnv(); |
| 157 | + env.view = ComponentContext.get(); |
| 158 | + env.envProxy = new LocalShadowObjectEnv(); |
| 159 | + |
| 160 | + // Create a hierarchy of view components |
| 161 | + const parentVC = new ViewComponent('parent2', {context: env.view}); |
| 162 | + const childVC = new ViewComponent('child', {context: env.view, parent: parentVC}); |
| 163 | + |
| 164 | + // Set up spies on ViewComponents to track event dispatch |
| 165 | + const parentSpy = vi.fn(); |
| 166 | + const childSpy = vi.fn(); |
| 167 | + |
| 168 | + on(parentVC, 'myEvent', parentSpy); |
| 169 | + on(childVC, 'myEvent', childSpy); |
| 170 | + |
| 171 | + // Define a shadow object that will dispatch the message without traverseChildren |
| 172 | + let entityRef: { |
| 173 | + dispatchMessageToView: (type: string, data?: unknown, transferables?: Transferable[], traverseChildren?: boolean) => void; |
| 174 | + } | null = null; |
| 175 | + |
| 176 | + @ShadowObject({token: 'parent2'}) |
| 177 | + class Parent2ShadowObject { |
| 178 | + constructor({entity}: ShadowObjectParams) { |
| 179 | + entityRef = entity; |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + expect(Parent2ShadowObject).toBeDefined(); |
| 184 | + |
| 185 | + await env.syncWait(); |
| 186 | + |
| 187 | + expect(entityRef).not.toBeNull(); |
| 188 | + |
| 189 | + // Dispatch a message with traverseChildren=false |
| 190 | + entityRef!.dispatchMessageToView('myEvent', {testData: 'world'}, undefined, false); |
| 191 | + |
| 192 | + // Wait for the microtask queue to process |
| 193 | + await new Promise((resolve) => setTimeout(resolve, 50)); |
| 194 | + |
| 195 | + // Verify the event was dispatched only to parent, not to child |
| 196 | + expect(parentSpy).toHaveBeenCalledTimes(1); |
| 197 | + expect(parentSpy).toHaveBeenCalledWith({testData: 'world'}); |
| 198 | + expect(childSpy).not.toHaveBeenCalled(); |
| 199 | + |
| 200 | + env.destroy(); |
| 201 | + }); |
90 | 202 | }); |
0 commit comments