import { expect, test, vi } from "vitest" import type { BuildEvents, ClassEventProxyOptions } from "#Source/event/index.ts" import { ClassEventProxy, EventManager } from "#Source/event/index.ts" type TestEvents = BuildEvents<{ change: (value: number) => void close: (reason: string) => void }> interface TestTarget { events: EventManager } const createTarget = (): TestTarget => { return { events: new EventManager() } } const createClassEventProxyHarness = (options?: { onSubscriberError?: (error: unknown) => void }): { targetA: TestTarget targetB: TestTarget classEventProxy: ClassEventProxy } => { const targetA = createTarget() const targetB = createTarget() const classEventProxyOptions: ClassEventProxyOptions = { targetAdapter: { emit: (target, event, ...args): boolean => { return target.events.emit(event, ...args) }, subscribe: (target, event, subscriber): void => { target.events.subscribe(event, subscriber) }, unsubscribe: (target, event, subscriber): void => { target.events.unsubscribe(event, subscriber) }, }, ...(options?.onSubscriberError === undefined ? {} : { onSubscriberError: (_subscriberEntry, error): void => { options.onSubscriberError?.(error) }, }), } const classEventProxy = new ClassEventProxy(classEventProxyOptions) return { targetA, targetB, classEventProxy, } } test("ClassEventProxy.has reports whether a managed subscriber exists on a target", () => { const { targetA, classEventProxy } = createClassEventProxyHarness() const subscriber = (value: number): void => { void value } expect(classEventProxy.has(targetA, "change", subscriber)).toBe(false) classEventProxy.subscribe(targetA, "change", subscriber) expect(classEventProxy.has(targetA, "change", subscriber)).toBe(true) classEventProxy.unsubscribe(targetA, "change", subscriber) expect(classEventProxy.has(targetA, "change", subscriber)).toBe(false) }) test("ClassEventProxy.subscribe creates one bridge per target event and returns a stable entry", () => { const { targetA, targetB, classEventProxy } = createClassEventProxyHarness() const subscribeSpy = vi.spyOn(targetA.events, "subscribe") const valuesA: number[] = [] const valuesB: number[] = [] const subscriberA = (value: number): void => { valuesA.push(value) } const subscriberB = (value: number): void => { valuesB.push(value) } const entryA = classEventProxy.subscribe(targetA, "change", subscriberA) const duplicateEntryA = classEventProxy.subscribe(targetA, "change", subscriberA) classEventProxy.subscribe(targetB, "change", subscriberB) expect(duplicateEntryA).toBe(entryA) expect(entryA.once).toBe(false) expect(subscribeSpy).toHaveBeenCalledTimes(1) expect(classEventProxy.emit(targetA, "change", 1)).toBe(true) expect(classEventProxy.emit(targetB, "change", 2)).toBe(true) expect(valuesA).toEqual([1]) expect(valuesB).toEqual([2]) }) test("ClassEventProxy.subscribeOnce removes the managed subscriber after the first bridge emission", () => { const handledErrors: unknown[] = [] const { targetA, classEventProxy } = createClassEventProxyHarness({ onSubscriberError: (error): void => { handledErrors.push(error) }, }) const onceSubscriber = (): void => { throw new Error("class-proxy-once-failed") } const entry = classEventProxy.subscribeOnce(targetA, "change", onceSubscriber) expect(entry.once).toBe(true) expect(classEventProxy.emit(targetA, "change", 1)).toBe(true) expect(classEventProxy.has(targetA, "change", onceSubscriber)).toBe(false) expect(handledErrors).toHaveLength(1) expect(targetA.events.emit("change", 2)).toBe(false) }) test("ClassEventProxy.unsubscribe removes only the specified managed subscriber", () => { const { targetA, classEventProxy } = createClassEventProxyHarness() const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe") const values: number[] = [] const keptSubscriber = (value: number): void => { values.push(value) } const removedSubscriber = (value: number): void => { values.push(value * 10) } classEventProxy.subscribe(targetA, "change", keptSubscriber) classEventProxy.subscribe(targetA, "change", removedSubscriber) expect(classEventProxy.unsubscribe(targetA, "change", removedSubscriber)).toBe(classEventProxy) expect(classEventProxy.emit(targetA, "change", 2)).toBe(true) expect(values).toEqual([2]) expect(unsubscribeSpy).not.toHaveBeenCalled() classEventProxy.unsubscribe(targetA, "change", keptSubscriber) expect(unsubscribeSpy).toHaveBeenCalledTimes(1) }) test("ClassEventProxy.removeSubscribersOfEvent clears one bridged event on one target", () => { const { targetA, classEventProxy } = createClassEventProxyHarness() const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe") const closeReasons: string[] = [] classEventProxy.subscribe(targetA, "change", () => undefined) classEventProxy.subscribe(targetA, "close", (reason) => { closeReasons.push(reason) }) expect(classEventProxy.removeSubscribersOfEvent(targetA, "change")).toBe(classEventProxy) expect(classEventProxy.emit(targetA, "change", 1)).toBe(false) expect(classEventProxy.emit(targetA, "close", "kept")).toBe(true) expect(closeReasons).toEqual(["kept"]) expect(unsubscribeSpy).toHaveBeenCalledTimes(1) }) test("ClassEventProxy.removeSubscribersOfTarget clears every bridged event of one target", () => { const { targetA, targetB, classEventProxy } = createClassEventProxyHarness() const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe") const keptValues: number[] = [] classEventProxy.subscribe(targetA, "change", () => undefined) classEventProxy.subscribe(targetA, "close", () => undefined) classEventProxy.subscribe(targetB, "change", (value) => { keptValues.push(value) }) expect(classEventProxy.removeSubscribersOfTarget(targetA)).toBe(classEventProxy) expect(classEventProxy.emit(targetA, "change", 1)).toBe(false) expect(classEventProxy.emit(targetB, "change", 2)).toBe(true) expect(keptValues).toEqual([2]) expect(unsubscribeSpy).toHaveBeenCalledTimes(2) }) test("ClassEventProxy.removeAllSubscribers clears every bridged target", () => { const { targetA, targetB, classEventProxy } = createClassEventProxyHarness() const subscriberA = (): void => undefined const subscriberB = (): void => undefined classEventProxy.subscribe(targetA, "change", subscriberA) classEventProxy.subscribe(targetB, "close", subscriberB) expect(classEventProxy.removeAllSubscribers()).toBe(classEventProxy) expect(classEventProxy.has(targetA, "change", subscriberA)).toBe(false) expect(classEventProxy.has(targetB, "close", subscriberB)).toBe(false) expect(classEventProxy.emit(targetA, "change", 1)).toBe(false) expect(classEventProxy.emit(targetB, "close", "done")).toBe(false) }) test("ClassEventProxy.emit delegates to the target adapter and handles managed subscriber errors", () => { const handledErrors: unknown[] = [] const { targetA, classEventProxy } = createClassEventProxyHarness({ onSubscriberError: (error): void => { handledErrors.push(error) }, }) const emitSpy = vi.spyOn(targetA.events, "emit") classEventProxy.subscribe(targetA, "close", () => { throw new Error("class-proxy-failed") }) expect(classEventProxy.emit(targetA, "close", "boom")).toBe(true) expect(emitSpy).toHaveBeenCalledTimes(1) expect(handledErrors).toHaveLength(1) const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) try { const { targetA: unmanagedTarget, classEventProxy: unmanagedClassEventProxy } = createClassEventProxyHarness() unmanagedClassEventProxy.subscribe(unmanagedTarget, "close", () => { throw new Error("class-proxy-unhandled") }) expect(unmanagedClassEventProxy.emit(unmanagedTarget, "close", "boom")).toBe(true) expect(consoleErrorSpy).toHaveBeenCalledTimes(1) } finally { consoleErrorSpy.mockRestore() } })