import { describe, it, expect, vi, beforeEach } from "vitest"; import { createWorkerHostEventTarget, createWorkerThreadEventTarget } from "../src/event-target-worker.js"; import type { EventEnvelope, TypedEvent, TypedEventTarget } from "../src/types.js"; type TestEvent = TypedEvent; function createMockWorker() { const posted: unknown[] = []; let onmessageHandler: ((event: MessageEvent) => void) | null = null; let onerrorHandler: ((event: ErrorEvent) => void) | null = null; const worker = { postMessage: vi.fn((data: unknown) => { posted.push(data); }), get onmessage() { return onmessageHandler; }, set onmessage(handler: ((event: MessageEvent) => void) | null) { onmessageHandler = handler; }, get onerror() { return onerrorHandler; }, set onerror(handler: ((event: ErrorEvent) => void) | null) { onerrorHandler = handler; }, posted, simulateMessage(data: unknown) { if (onmessageHandler) { onmessageHandler({ data } as MessageEvent); } }, }; Object.defineProperty(worker, "onmessage", { get() { return onmessageHandler; }, set(handler: ((event: MessageEvent) => void) | null) { onmessageHandler = handler; }, }); Object.defineProperty(worker, "onerror", { get() { return onerrorHandler; }, set(handler: ((event: ErrorEvent) => void) | null) { onerrorHandler = handler; }, }); return worker; } function createMockGlobalThis() { const posted: unknown[] = []; let onmessageHandler: ((event: MessageEvent) => void) | null = null; const global = { onmessage: null as ((event: MessageEvent) => void) | null, postMessage: vi.fn((data: unknown) => { posted.push(data); }), posted, simulateMessage(data: unknown) { if (onmessageHandler) { onmessageHandler({ data } as MessageEvent); } }, }; Object.defineProperty(global, "onmessage", { get() { return onmessageHandler; }, set(handler: ((event: MessageEvent) => void) | null) { onmessageHandler = handler; }, }); return global; } describe("createWorkerHostEventTarget", () => { describe("dispatchEvent (send path)", () => { it("posts event.detail to worker via worker.postMessage", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const envelope: EventEnvelope<"call.responded", { status: string }> = { type: "call.responded", id: "uuid-123", payload: { status: "ok" }, }; const event = new CustomEvent("call.responded:uuid-123", { detail: envelope, }) as TestEvent; eventTarget.dispatchEvent(event); expect(worker.postMessage).toHaveBeenCalledTimes(1); expect(worker.postMessage).toHaveBeenCalledWith(envelope); }); it("returns true from dispatchEvent", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const event = new CustomEvent("test:event", { detail: { type: "test", id: "event", payload: null }, }) as TestEvent; const result = eventTarget.dispatchEvent(event); expect(result).toBe(true); }); it("posts envelope with null payload", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const event = new CustomEvent("notify:1", { detail: { type: "notify", id: "1", payload: null }, }) as TestEvent; eventTarget.dispatchEvent(event); expect(worker.posted[0]).toEqual({ type: "notify", id: "1", payload: null }); }); it("posts multiple events in order", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const event1 = new CustomEvent("a:1", { detail: { type: "a", id: "1", payload: "first" } }) as TestEvent; const event2 = new CustomEvent("b:2", { detail: { type: "b", id: "2", payload: "second" } }) as TestEvent; eventTarget.dispatchEvent(event1); eventTarget.dispatchEvent(event2); expect(worker.posted).toHaveLength(2); expect(worker.posted[0]).toEqual({ type: "a", id: "1", payload: "first" }); expect(worker.posted[1]).toEqual({ type: "b", id: "2", payload: "second" }); }); }); describe("addEventListener (subscribe path)", () => { it("registers a listener for a topic", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("call.responded:uuid-123", listener); const envelope: EventEnvelope = { type: "call.responded", id: "uuid-123", payload: "hello" }; worker.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); }); it("does nothing when callback is null", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); eventTarget.addEventListener("topic:a", null as any); const envelope: EventEnvelope = { type: "topic", id: "a", payload: null }; worker.simulateMessage(envelope); }); it("supports EventListenerObject with handleEvent", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const handleEvent = vi.fn(); const listenerObject = { handleEvent }; eventTarget.addEventListener("obj:test", listenerObject); const envelope: EventEnvelope = { type: "obj", id: "test", payload: null }; worker.simulateMessage(envelope); expect(handleEvent).toHaveBeenCalledTimes(1); }); it("supports multiple listeners on the same topic", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener1 = vi.fn(); const listener2 = vi.fn(); eventTarget.addEventListener("topic:x", listener1); eventTarget.addEventListener("topic:x", listener2); const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" }; worker.simulateMessage(envelope); expect(listener1).toHaveBeenCalledTimes(1); expect(listener2).toHaveBeenCalledTimes(1); }); }); describe("removeEventListener (unsubscribe path)", () => { it("removes a listener so it no longer receives events", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); eventTarget.removeEventListener("topic:a", listener); const envelope: EventEnvelope = { type: "topic", id: "a", payload: null }; worker.simulateMessage(envelope); expect(listener).not.toHaveBeenCalled(); }); it("removes only the specified listener, keeping others", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener1 = vi.fn(); const listener2 = vi.fn(); eventTarget.addEventListener("topic:a", listener1); eventTarget.addEventListener("topic:a", listener2); eventTarget.removeEventListener("topic:a", listener1); const envelope: EventEnvelope = { type: "topic", id: "a", payload: "data" }; worker.simulateMessage(envelope); expect(listener1).not.toHaveBeenCalled(); expect(listener2).toHaveBeenCalledTimes(1); }); it("supports EventListenerObject removal", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const handleEvent = vi.fn(); const listenerObject = { handleEvent }; eventTarget.addEventListener("obj:test", listenerObject); eventTarget.removeEventListener("obj:test", listenerObject); const envelope: EventEnvelope = { type: "obj", id: "test", payload: null }; worker.simulateMessage(envelope); expect(handleEvent).not.toHaveBeenCalled(); }); it("does nothing when removing a callback that was never registered", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const unregisteredListener = vi.fn(); eventTarget.removeEventListener("topic:a", unregisteredListener); expect(() => eventTarget.removeEventListener("topic:a", unregisteredListener)).not.toThrow(); }); it("does nothing when callback is null", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); eventTarget.removeEventListener("topic:a", null as any); }); }); describe("receive path (worker.onmessage)", () => { it("parses envelope from event.data, creates CustomEvent with type:id topic, and dispatches to listeners", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("message.sent:msg1", listener); const envelope: EventEnvelope = { type: "message.sent", id: "msg1", payload: "hello world" }; worker.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); const receivedEvent = listener.mock.calls[0][0] as TestEvent; expect(receivedEvent.type).toBe("message.sent:msg1"); expect(receivedEvent.detail).toEqual(envelope); }); it("delivers messages to all listeners on the same topic", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener1 = vi.fn(); const listener2 = vi.fn(); eventTarget.addEventListener("topic:x", listener1); eventTarget.addEventListener("topic:x", listener2); const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" }; worker.simulateMessage(envelope); expect(listener1).toHaveBeenCalledTimes(1); expect(listener2).toHaveBeenCalledTimes(1); expect((listener1.mock.calls[0][0] as TestEvent).detail).toEqual(envelope); expect((listener2.mock.calls[0][0] as TestEvent).detail).toEqual(envelope); }); it("ignores messages for topics with no registered listeners", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); const envelope: EventEnvelope = { type: "other", id: "b", payload: null }; worker.simulateMessage(envelope); expect(listener).not.toHaveBeenCalled(); }); }); describe("malformed message handling", () => { it("ignores messages where envelope.type is not a string", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); worker.simulateMessage({ type: 123, id: "a", payload: null }); expect(listener).not.toHaveBeenCalled(); }); it("ignores messages where envelope is null", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); worker.simulateMessage(null); expect(listener).not.toHaveBeenCalled(); }); it("ignores messages where envelope is undefined", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); worker.simulateMessage(undefined); expect(listener).not.toHaveBeenCalled(); }); it("ignores messages where envelope.type starts with __", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("__custom:thing", listener); worker.simulateMessage({ type: "__custom", id: "thing", payload: null }); expect(listener).not.toHaveBeenCalled(); }); }); describe("topic scoping", () => { it("forms topic from envelope type and id fields", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("user.action:abc-123", listener); const envelope: EventEnvelope = { type: "user.action", id: "abc-123", payload: { done: true } }; worker.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); expect((listener.mock.calls[0][0] as TestEvent).type).toBe("user.action:abc-123"); }); it("does not match when type differs even if id is the same", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("user.created:id1", listener); worker.simulateMessage({ type: "user.deleted", id: "id1", payload: null }); expect(listener).not.toHaveBeenCalled(); }); it("does not match when id differs even if type is the same", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("event:alpha", listener); worker.simulateMessage({ type: "event", id: "beta", payload: null }); expect(listener).not.toHaveBeenCalled(); }); }); describe("EventEnvelope round-trip", () => { it("round-trips full { type, id, payload } envelope through dispatch and receive", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("user.joined:user-99", listener); const originalEnvelope: EventEnvelope<"user.joined", { name: string; role: string }> = { type: "user.joined", id: "user-99", payload: { name: "Bob", role: "admin" }, }; const event = new CustomEvent("user.joined:user-99", { detail: originalEnvelope, }) as TestEvent; eventTarget.dispatchEvent(event); const dispatchedData = worker.posted[0]; expect(dispatchedData).toEqual(originalEnvelope); worker.simulateMessage(dispatchedData); expect(listener).toHaveBeenCalledTimes(1); const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope; expect(receivedDetail).toEqual(originalEnvelope); expect(receivedDetail.type).toBe("user.joined"); expect(receivedDetail.id).toBe("user-99"); expect(receivedDetail.payload).toEqual({ name: "Bob", role: "admin" }); }); it("round-trips envelope with null payload", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("ping:1", listener); const envelope: EventEnvelope = { type: "ping", id: "1", payload: null }; worker.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope; expect(receivedDetail).toEqual(envelope); }); }); describe("worker.onerror", () => { it("does not propagate worker errors to event target listeners", () => { const worker = createMockWorker(); const eventTarget = createWorkerHostEventTarget(worker as any); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); if (worker.onerror) { worker.onerror(new ErrorEvent("error", { message: "Worker failed" })); } expect(listener).not.toHaveBeenCalled(); }); }); }); describe("createWorkerThreadEventTarget", () => { let originalGlobalThis: typeof globalThis; let mockGlobal: ReturnType; beforeEach(() => { originalGlobalThis = globalThis; mockGlobal = createMockGlobalThis(); }); function createThreadEventTargetWithMock() { const callbacksForTopic = new Map>(); mockGlobal.onmessage = (event: MessageEvent) => { const envelope = event.data as EventEnvelope; if (typeof envelope?.type !== "string" || envelope.type.startsWith("__")) { return; } const topic = `${envelope.type}:${envelope.id}`; const callbacks = callbacksForTopic.get(topic); if (callbacks === undefined) { return; } const customEvent = new CustomEvent(topic, { detail: envelope, }) as TestEvent; for (const callback of callbacks) { callback(customEvent); } }; function addCallback(topic: string, callback: EventListener) { let callbacks = callbacksForTopic.get(topic); if (callbacks === undefined) { callbacks = new Set(); callbacksForTopic.set(topic, callbacks); } callbacks.add(callback); } function removeCallback(topic: string, callback: EventListener) { const callbacks = callbacksForTopic.get(topic); if (callbacks === undefined) { return; } callbacks.delete(callback); if (callbacks.size === 0) { callbacksForTopic.delete(topic); } } return { eventTarget: { addEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) { if (callbackOrOptions != null) { const callback = "handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions; addCallback(topic, callback); } }, dispatchEvent(event: TestEvent) { mockGlobal.postMessage(event.detail); return true; }, removeEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) { if (callbackOrOptions != null) { const callback = "handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions; removeCallback(topic, callback); } }, } as TypedEventTarget, callbacksForTopic, }; } describe("dispatchEvent (send path)", () => { it("posts event.detail via globalThis.postMessage", () => { const { eventTarget } = createThreadEventTargetWithMock(); const envelope: EventEnvelope<"call.responded", { status: string }> = { type: "call.responded", id: "uuid-123", payload: { status: "ok" }, }; const event = new CustomEvent("call.responded:uuid-123", { detail: envelope, }) as TestEvent; eventTarget.dispatchEvent(event); expect(mockGlobal.postMessage).toHaveBeenCalledTimes(1); expect(mockGlobal.postMessage).toHaveBeenCalledWith(envelope); }); it("returns true from dispatchEvent", () => { const { eventTarget } = createThreadEventTargetWithMock(); const event = new CustomEvent("test:event", { detail: { type: "test", id: "event", payload: null }, }) as TestEvent; const result = eventTarget.dispatchEvent(event); expect(result).toBe(true); }); it("posts envelope with null payload", () => { const { eventTarget } = createThreadEventTargetWithMock(); const event = new CustomEvent("notify:1", { detail: { type: "notify", id: "1", payload: null }, }) as TestEvent; eventTarget.dispatchEvent(event); expect(mockGlobal.posted[0]).toEqual({ type: "notify", id: "1", payload: null }); }); }); describe("receive path (globalThis.onmessage)", () => { it("parses envelope from event.data, creates CustomEvent with type:id topic, and dispatches to listeners", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("message.sent:msg1", listener); const envelope: EventEnvelope = { type: "message.sent", id: "msg1", payload: "hello" }; mockGlobal.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); const receivedEvent = listener.mock.calls[0][0] as TestEvent; expect(receivedEvent.type).toBe("message.sent:msg1"); expect(receivedEvent.detail).toEqual(envelope); }); it("delivers messages to all listeners on the same topic", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener1 = vi.fn(); const listener2 = vi.fn(); eventTarget.addEventListener("topic:x", listener1); eventTarget.addEventListener("topic:x", listener2); const envelope: EventEnvelope = { type: "topic", id: "x", payload: "data" }; mockGlobal.simulateMessage(envelope); expect(listener1).toHaveBeenCalledTimes(1); expect(listener2).toHaveBeenCalledTimes(1); }); it("ignores messages for topics with no registered listeners", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); const envelope: EventEnvelope = { type: "other", id: "b", payload: null }; mockGlobal.simulateMessage(envelope); expect(listener).not.toHaveBeenCalled(); }); }); describe("addEventListener", () => { it("supports EventListenerObject with handleEvent", () => { const { eventTarget } = createThreadEventTargetWithMock(); const handleEvent = vi.fn(); const listenerObject = { handleEvent }; eventTarget.addEventListener("obj:test", listenerObject); const envelope: EventEnvelope = { type: "obj", id: "test", payload: null }; mockGlobal.simulateMessage(envelope); expect(handleEvent).toHaveBeenCalledTimes(1); }); }); describe("removeEventListener", () => { it("removes a listener so it no longer receives events", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); eventTarget.removeEventListener("topic:a", listener); const envelope: EventEnvelope = { type: "topic", id: "a", payload: null }; mockGlobal.simulateMessage(envelope); expect(listener).not.toHaveBeenCalled(); }); it("supports EventListenerObject removal", () => { const { eventTarget } = createThreadEventTargetWithMock(); const handleEvent = vi.fn(); const listenerObject = { handleEvent }; eventTarget.addEventListener("obj:test", listenerObject); eventTarget.removeEventListener("obj:test", listenerObject); const envelope: EventEnvelope = { type: "obj", id: "test", payload: null }; mockGlobal.simulateMessage(envelope); expect(handleEvent).not.toHaveBeenCalled(); }); }); describe("malformed message handling", () => { it("ignores messages where envelope.type is not a string", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); mockGlobal.simulateMessage({ type: 123, id: "a", payload: null }); expect(listener).not.toHaveBeenCalled(); }); it("ignores messages where envelope is null", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("topic:a", listener); mockGlobal.simulateMessage(null); expect(listener).not.toHaveBeenCalled(); }); it("ignores messages where envelope.type starts with __", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("__custom:thing", listener); mockGlobal.simulateMessage({ type: "__custom", id: "thing", payload: null }); expect(listener).not.toHaveBeenCalled(); }); }); describe("EventEnvelope round-trip", () => { it("round-trips full envelope through dispatch and receive", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("user.joined:user-99", listener); const originalEnvelope: EventEnvelope<"user.joined", { name: string; role: string }> = { type: "user.joined", id: "user-99", payload: { name: "Bob", role: "admin" }, }; const event = new CustomEvent("user.joined:user-99", { detail: originalEnvelope, }) as TestEvent; eventTarget.dispatchEvent(event); const dispatchedData = mockGlobal.posted[0]; expect(dispatchedData).toEqual(originalEnvelope); mockGlobal.simulateMessage(dispatchedData); expect(listener).toHaveBeenCalledTimes(1); const receivedDetail = (listener.mock.calls[0][0] as TestEvent).detail as EventEnvelope; expect(receivedDetail).toEqual(originalEnvelope); }); }); describe("topic scoping", () => { it("forms topic from envelope type and id fields", () => { const { eventTarget } = createThreadEventTargetWithMock(); const listener = vi.fn(); eventTarget.addEventListener("user.action:abc-123", listener); const envelope: EventEnvelope = { type: "user.action", id: "abc-123", payload: { done: true } }; mockGlobal.simulateMessage(envelope); expect(listener).toHaveBeenCalledTimes(1); expect((listener.mock.calls[0][0] as TestEvent).type).toBe("user.action:abc-123"); }); }); }); describe("bidirectional communication (host + thread)", () => { it("host sends envelope that thread receives", () => { const worker = createMockWorker(); const hostTarget = createWorkerHostEventTarget(worker as any); let threadOnmessage: ((event: { data: unknown }) => void) | null = null; const threadCallbacks = new Map>(); const threadTarget = { addEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) { if (callbackOrOptions != null) { const callback = "handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions; let callbacks = threadCallbacks.get(topic); if (callbacks === undefined) { callbacks = new Set(); threadCallbacks.set(topic, callbacks); } callbacks.add(callback); } }, dispatchEvent(event: TestEvent) { return true; }, removeEventListener(topic: string, callbackOrOptions: EventListenerOrEventListenerObject) { if (callbackOrOptions != null) { const callback = "handleEvent" in callbackOrOptions ? callbackOrOptions.handleEvent : callbackOrOptions; const callbacks = threadCallbacks.get(topic); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { threadCallbacks.delete(topic); } } } }, } as TypedEventTarget; threadOnmessage = (event: { data: unknown }) => { const envelope = event.data as EventEnvelope; if (typeof envelope?.type !== "string" || envelope.type.startsWith("__")) { return; } const topic = `${envelope.type}:${envelope.id}`; const callbacks = threadCallbacks.get(topic); if (callbacks === undefined) return; const customEvent = new CustomEvent(topic, { detail: envelope }) as TestEvent; for (const callback of callbacks) { callback(customEvent); } }; const threadListener = vi.fn(); threadTarget.addEventListener("task.assigned:task-1", threadListener); const envelope: EventEnvelope = { type: "task.assigned", id: "task-1", payload: { work: "compute" } }; const hostEvent = new CustomEvent("task.assigned:task-1", { detail: envelope }) as TestEvent; hostTarget.dispatchEvent(hostEvent); expect(worker.postMessage).toHaveBeenCalledWith(envelope); const postedEnvelope = worker.posted[0] as EventEnvelope; threadOnmessage!({ data: postedEnvelope }); expect(threadListener).toHaveBeenCalledTimes(1); const receivedDetail = (threadListener.mock.calls[0][0] as TestEvent).detail as EventEnvelope; expect(receivedDetail).toEqual(envelope); }); it("thread sends envelope that host receives", () => { const worker = createMockWorker(); const hostTarget = createWorkerHostEventTarget(worker as any); const hostListener = vi.fn(); hostTarget.addEventListener("result.ready:res-1", hostListener); const envelope: EventEnvelope = { type: "result.ready", id: "res-1", payload: { output: 42 } }; worker.simulateMessage(envelope); expect(hostListener).toHaveBeenCalledTimes(1); const receivedDetail = (hostListener.mock.calls[0][0] as TestEvent).detail as EventEnvelope; expect(receivedDetail).toEqual(envelope); }); });