import { expect, test, vi } from "vitest" import type { BuildEvents, InstanceEventProxyOptions } from "#Source/event/index.ts" import { EventManager, InstanceEventProxy } from "#Source/event/index.ts" type TestEvents = BuildEvents<{ change: (value: number) => void error: (message: string) => void }> const createInstanceEventProxyHarness = (options?: { onSubscriberError?: (error: unknown) => void }): { target: EventManager eventProxy: InstanceEventProxy } => { const target = new EventManager() const eventProxyOptions: InstanceEventProxyOptions = { targetAdapter: { emit: (event, ...args): boolean => { return target.emit(event, ...args) }, subscribe: (event, subscriber): void => { target.subscribe(event, subscriber) }, unsubscribe: (event, subscriber): void => { target.unsubscribe(event, subscriber) }, }, ...(options?.onSubscriberError === undefined ? {} : { onSubscriberError: (_subscriberEntry, error): void => { options.onSubscriberError?.(error) }, }), } const eventProxy = new InstanceEventProxy(eventProxyOptions) return { target, eventProxy, } } test("InstanceEventProxy.has reports whether a managed subscriber exists", () => { const { eventProxy } = createInstanceEventProxyHarness() const subscriber = (value: number): void => { void value } expect(eventProxy.has("change", subscriber)).toBe(false) eventProxy.subscribe("change", subscriber) expect(eventProxy.has("change", subscriber)).toBe(true) eventProxy.unsubscribe("change", subscriber) expect(eventProxy.has("change", subscriber)).toBe(false) }) test("InstanceEventProxy.subscribe creates one bridge subscriber and returns a stable entry", () => { const { target, eventProxy } = createInstanceEventProxyHarness() const subscribeSpy = vi.spyOn(target, "subscribe") const values: number[] = [] const subscriber = (value: number): void => { values.push(value) } const firstEntry = eventProxy.subscribe("change", subscriber) const duplicateEntry = eventProxy.subscribe("change", subscriber) expect(duplicateEntry).toBe(firstEntry) expect(firstEntry.once).toBe(false) expect(subscribeSpy).toHaveBeenCalledTimes(1) expect(eventProxy.emit("change", 1)).toBe(true) expect(values).toEqual([1]) }) test("InstanceEventProxy.subscribeOnce removes the managed subscriber after the first bridge emission", () => { const handledErrors: unknown[] = [] const { eventProxy, target } = createInstanceEventProxyHarness({ onSubscriberError: (error): void => { handledErrors.push(error) }, }) const onceSubscriber = (): void => { throw new Error("proxy-once-failed") } const entry = eventProxy.subscribeOnce("change", onceSubscriber) expect(entry.once).toBe(true) expect(eventProxy.emit("change", 1)).toBe(true) expect(eventProxy.has("change", onceSubscriber)).toBe(false) expect(handledErrors).toHaveLength(1) expect(target.emit("change", 2)).toBe(false) }) test("InstanceEventProxy.unsubscribe removes only the specified managed subscriber", () => { const { target, eventProxy } = createInstanceEventProxyHarness() const unsubscribeSpy = vi.spyOn(target, "unsubscribe") const values: number[] = [] const keptSubscriber = (value: number): void => { values.push(value) } const removedSubscriber = (value: number): void => { values.push(value * 10) } eventProxy.subscribe("change", keptSubscriber) eventProxy.subscribe("change", removedSubscriber) expect(eventProxy.unsubscribe("change", removedSubscriber)).toBe(eventProxy) expect(eventProxy.emit("change", 2)).toBe(true) expect(values).toEqual([2]) expect(unsubscribeSpy).not.toHaveBeenCalled() eventProxy.unsubscribe("change", keptSubscriber) expect(unsubscribeSpy).toHaveBeenCalledTimes(1) }) test("InstanceEventProxy.removeSubscribersOfEvent clears one bridged event", () => { const { target, eventProxy } = createInstanceEventProxyHarness() const unsubscribeSpy = vi.spyOn(target, "unsubscribe") const errors: string[] = [] eventProxy.subscribe("change", () => undefined) eventProxy.subscribe("error", (message) => { errors.push(message) }) expect(eventProxy.removeSubscribersOfEvent("change")).toBe(eventProxy) expect(eventProxy.emit("change", 1)).toBe(false) expect(eventProxy.emit("error", "kept")).toBe(true) expect(errors).toEqual(["kept"]) expect(unsubscribeSpy).toHaveBeenCalledTimes(1) }) test("InstanceEventProxy.removeAllSubscribers clears every bridged event", () => { const { target, eventProxy } = createInstanceEventProxyHarness() const unsubscribeSpy = vi.spyOn(target, "unsubscribe") eventProxy.subscribe("change", () => undefined) eventProxy.subscribe("error", () => undefined) expect(eventProxy.removeAllSubscribers()).toBe(eventProxy) expect(eventProxy.emit("change", 1)).toBe(false) expect(eventProxy.emit("error", "boom")).toBe(false) expect(unsubscribeSpy).toHaveBeenCalledTimes(2) }) test("InstanceEventProxy.emit delegates to the target adapter and handles managed subscriber errors", () => { const handledErrors: unknown[] = [] const { target, eventProxy } = createInstanceEventProxyHarness({ onSubscriberError: (error): void => { handledErrors.push(error) }, }) const emitSpy = vi.spyOn(target, "emit") eventProxy.subscribe("change", () => { throw new Error("proxy-failed") }) expect(eventProxy.emit("change", 1)).toBe(true) expect(emitSpy).toHaveBeenCalledTimes(1) expect(handledErrors).toHaveLength(1) const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) try { const { eventProxy: unmanagedEventProxy } = createInstanceEventProxyHarness() unmanagedEventProxy.subscribe("error", () => { throw new Error("proxy-unhandled") }) expect(unmanagedEventProxy.emit("error", "boom")).toBe(true) expect(consoleErrorSpy).toHaveBeenCalledTimes(1) } finally { consoleErrorSpy.mockRestore() } })