Skip to content

Commit 06d3e72

Browse files
Copilotspearwolf
andauthored
Add tests for MessageToView#traverseChildren implementation (#1)
* Initial plan * Add tests for MessageToView#traverseChildren implementation Co-authored-by: spearwolf <12805+spearwolf@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: spearwolf <12805+spearwolf@users.noreply.github.com>
1 parent 67f132e commit 06d3e72

4 files changed

Lines changed: 285 additions & 6 deletions

File tree

packages/shadow-objects/src/in-the-dark/Kernel.spec.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import {afterEach, describe, expect, it} from 'vitest';
1+
import {on} from '@spearwolf/eventize';
2+
import {afterEach, describe, expect, it, vi} from 'vitest';
3+
import {MessageToView} from '../constants.js';
24
import {generateUUID} from '../utils/generateUUID.js';
3-
import {Kernel} from './Kernel.js';
5+
import {Kernel, type MessageToViewEvent} from './Kernel.js';
46
import {Registry} from './Registry.js';
57
import {ShadowObject} from './ShadowObject.js';
68

@@ -159,4 +161,85 @@ describe('Kernel', () => {
159161
'should contain instanceof ObersteDirektive',
160162
).toBeDefined();
161163
});
164+
165+
describe('MessageToView with traverseChildren', () => {
166+
it('should emit MessageToView event with traverseChildren=false by default', async () => {
167+
const kernel = new Kernel();
168+
const uuid = generateUUID();
169+
170+
kernel.createEntity(uuid, 'test');
171+
const entity = kernel.getEntity(uuid);
172+
173+
const messageToViewSpy = vi.fn();
174+
on(kernel, MessageToView, messageToViewSpy);
175+
176+
entity.dispatchMessageToView('testType', {payload: 'data'});
177+
178+
// Wait for queueMicrotask to complete
179+
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
180+
181+
expect(messageToViewSpy).toHaveBeenCalledTimes(1);
182+
183+
const message: MessageToViewEvent = messageToViewSpy.mock.calls[0][0];
184+
expect(message.uuid).toBe(uuid);
185+
expect(message.type).toBe('testType');
186+
expect(message.data).toEqual({payload: 'data'});
187+
expect(message.traverseChildren).toBe(false);
188+
189+
kernel.destroy();
190+
});
191+
192+
it('should emit MessageToView event with traverseChildren=true when specified', async () => {
193+
const kernel = new Kernel();
194+
const uuid = generateUUID();
195+
196+
kernel.createEntity(uuid, 'test');
197+
const entity = kernel.getEntity(uuid);
198+
199+
const messageToViewSpy = vi.fn();
200+
on(kernel, MessageToView, messageToViewSpy);
201+
202+
entity.dispatchMessageToView('broadcastEvent', {message: 'hello'}, undefined, true);
203+
204+
// Wait for queueMicrotask to complete
205+
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
206+
207+
expect(messageToViewSpy).toHaveBeenCalledTimes(1);
208+
209+
const message: MessageToViewEvent = messageToViewSpy.mock.calls[0][0];
210+
expect(message.uuid).toBe(uuid);
211+
expect(message.type).toBe('broadcastEvent');
212+
expect(message.data).toEqual({message: 'hello'});
213+
expect(message.traverseChildren).toBe(true);
214+
215+
kernel.destroy();
216+
});
217+
218+
it('should emit MessageToView with transferables', async () => {
219+
const kernel = new Kernel();
220+
const uuid = generateUUID();
221+
222+
kernel.createEntity(uuid, 'test');
223+
const entity = kernel.getEntity(uuid);
224+
225+
const messageToViewSpy = vi.fn();
226+
on(kernel, MessageToView, messageToViewSpy);
227+
228+
const buffer = new ArrayBuffer(8);
229+
entity.dispatchMessageToView('dataEvent', {buffer}, [buffer], false);
230+
231+
// Wait for queueMicrotask to complete
232+
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
233+
234+
expect(messageToViewSpy).toHaveBeenCalledTimes(1);
235+
236+
const message: MessageToViewEvent = messageToViewSpy.mock.calls[0][0];
237+
expect(message.uuid).toBe(uuid);
238+
expect(message.type).toBe('dataEvent');
239+
expect(message.transferables).toContain(buffer);
240+
expect(message.traverseChildren).toBe(false);
241+
242+
kernel.destroy();
243+
});
244+
});
162245
});

packages/shadow-objects/src/in-the-dark/Kernel.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export interface MessageToViewEvent {
2525
data?: unknown;
2626
transferables?: Transferable[];
2727
traverseChildren?: boolean;
28-
// TODO(test) if MessageToView#traverseChildren is implemented all the way down
2928
}
3029

3130
interface EntityEntry {
@@ -383,7 +382,11 @@ export class Kernel {
383382
return ctxProvider;
384383
},
385384

386-
provideGlobalContext<T = unknown>(name: string | symbol, sourceOrInitialValue?: T | SignalReader<T>, isEqual?: CompareFunc<T>) {
385+
provideGlobalContext<T = unknown>(
386+
name: string | symbol,
387+
sourceOrInitialValue?: T | SignalReader<T>,
388+
isEqual?: CompareFunc<T>,
389+
) {
387390
let ctxProvider = contextRootProviders.get(name);
388391

389392
if (ctxProvider == null) {

packages/shadow-objects/src/view/ShadowEnv.spec.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {once} from '@spearwolf/eventize';
1+
import {on, once} from '@spearwolf/eventize';
22
import {afterEach, describe, expect, it, vi} from 'vitest';
33
import {Registry} from '../in-the-dark/Registry.js';
44
import {ShadowObject} from '../in-the-dark/ShadowObject.js';
5+
import type {ShadowObjectParams} from '../types.js';
56
import {ComponentContext} from './ComponentContext.js';
67
import {LocalShadowObjectEnv} from './LocalShadowObjectEnv.js';
78
import {ShadowEnv} from './ShadowEnv.js';
@@ -87,4 +88,115 @@ describe('ShadowEnv', () => {
8788

8889
env.destroy();
8990
});
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+
});
90202
});

packages/shadow-objects/src/view/ViewComponent.spec.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {afterEach, describe, expect, it} from 'vitest';
1+
import {on} from '@spearwolf/eventize';
2+
import {afterEach, describe, expect, it, vi} from 'vitest';
23
import {ComponentContext} from './ComponentContext.js';
34
import {ViewComponent} from './ViewComponent.js';
45

@@ -143,4 +144,84 @@ describe('ViewComponent', () => {
143144

144145
otherCtx.clear();
145146
});
147+
148+
it('should dispatch event without traverseChildren', () => {
149+
const parent = new ViewComponent('parent');
150+
const child = new ViewComponent('child', parent);
151+
const grandChild = new ViewComponent('grandChild', child);
152+
153+
const parentSpy = vi.fn();
154+
const childSpy = vi.fn();
155+
const grandChildSpy = vi.fn();
156+
157+
on(parent, 'testEvent', parentSpy);
158+
on(child, 'testEvent', childSpy);
159+
on(grandChild, 'testEvent', grandChildSpy);
160+
161+
parent.dispatchEvent('testEvent', {foo: 'bar'}, false);
162+
163+
expect(parentSpy).toHaveBeenCalledTimes(1);
164+
expect(parentSpy).toHaveBeenCalledWith({foo: 'bar'});
165+
expect(childSpy).not.toHaveBeenCalled();
166+
expect(grandChildSpy).not.toHaveBeenCalled();
167+
});
168+
169+
it('should dispatch event with traverseChildren=true to all descendants', () => {
170+
const parent = new ViewComponent('parent');
171+
const child1 = new ViewComponent('child1', parent);
172+
const child2 = new ViewComponent('child2', parent);
173+
const grandChild1 = new ViewComponent('grandChild1', child1);
174+
const grandChild2 = new ViewComponent('grandChild2', child1);
175+
176+
const parentSpy = vi.fn();
177+
const child1Spy = vi.fn();
178+
const child2Spy = vi.fn();
179+
const grandChild1Spy = vi.fn();
180+
const grandChild2Spy = vi.fn();
181+
182+
on(parent, 'testEvent', parentSpy);
183+
on(child1, 'testEvent', child1Spy);
184+
on(child2, 'testEvent', child2Spy);
185+
on(grandChild1, 'testEvent', grandChild1Spy);
186+
on(grandChild2, 'testEvent', grandChild2Spy);
187+
188+
parent.dispatchEvent('testEvent', {data: 123}, true);
189+
190+
expect(parentSpy).toHaveBeenCalledTimes(1);
191+
expect(parentSpy).toHaveBeenCalledWith({data: 123});
192+
193+
expect(child1Spy).toHaveBeenCalledTimes(1);
194+
expect(child1Spy).toHaveBeenCalledWith({data: 123});
195+
196+
expect(child2Spy).toHaveBeenCalledTimes(1);
197+
expect(child2Spy).toHaveBeenCalledWith({data: 123});
198+
199+
expect(grandChild1Spy).toHaveBeenCalledTimes(1);
200+
expect(grandChild1Spy).toHaveBeenCalledWith({data: 123});
201+
202+
expect(grandChild2Spy).toHaveBeenCalledTimes(1);
203+
expect(grandChild2Spy).toHaveBeenCalledWith({data: 123});
204+
});
205+
206+
it('should dispatch event from child with traverseChildren=true without affecting parent', () => {
207+
const parent = new ViewComponent('parent');
208+
const child = new ViewComponent('child', parent);
209+
const grandChild = new ViewComponent('grandChild', child);
210+
211+
const parentSpy = vi.fn();
212+
const childSpy = vi.fn();
213+
const grandChildSpy = vi.fn();
214+
215+
on(parent, 'testEvent', parentSpy);
216+
on(child, 'testEvent', childSpy);
217+
on(grandChild, 'testEvent', grandChildSpy);
218+
219+
child.dispatchEvent('testEvent', 'hello', true);
220+
221+
expect(parentSpy).not.toHaveBeenCalled();
222+
expect(childSpy).toHaveBeenCalledTimes(1);
223+
expect(childSpy).toHaveBeenCalledWith('hello');
224+
expect(grandChildSpy).toHaveBeenCalledTimes(1);
225+
expect(grandChildSpy).toHaveBeenCalledWith('hello');
226+
});
146227
});

0 commit comments

Comments
 (0)