import { describe, expect, it, vi } from "vitest"; import { RuntimeEvents } from "@/kernel/RuntimeEvents"; import { AppRuntime } from "@/kernel/AppRuntime"; import type { RuntimePlugin } from "@/kernel/RuntimePlugin"; vi.mock("@/core/Game", () => { class MockGame { private static instance: MockGame; static getInstance(): MockGame { if (!MockGame.instance) { MockGame.instance = new MockGame(); } return MockGame.instance; } } return { default: MockGame }; }); vi.mock("@/scene/SceneManager", () => { class MockSceneManager { private static instance: MockSceneManager; static getInstance(): MockSceneManager { if (!MockSceneManager.instance) { MockSceneManager.instance = new MockSceneManager(); } return MockSceneManager.instance; } } return { default: MockSceneManager }; }); describe("RuntimeEvents", () => { it("supports on/off/emit with variadic args", () => { const events = new RuntimeEvents(); const handler = vi.fn(); events.on("runtime:ready", handler); events.emit("runtime:ready", { startedAt: 1 }, "extra", 42); events.off("runtime:ready", handler); events.emit("runtime:ready", { startedAt: 2 }); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith({ startedAt: 1 }, "extra", 42); }); it("supports once subscriptions", () => { const events = new RuntimeEvents(); const handler = vi.fn(); events.once("scene:changed", handler); events.emit("scene:changed", { current: "a", previous: undefined }); events.emit("scene:changed", { current: "b", previous: "a" }); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith({ current: "a", previous: undefined }); }); it("keeps dispatching when one listener throws", () => { const events = new RuntimeEvents(); const bad = vi.fn(() => { throw new Error("boom"); }); const good = vi.fn(); events.on("scene:changed", bad); events.on("scene:changed", good); events.emit("scene:changed", { current: "x" }); expect(bad).toHaveBeenCalledTimes(1); expect(good).toHaveBeenCalledTimes(1); expect(good).toHaveBeenCalledWith({ current: "x" }); }); it("unsubscribes once listener even when callback throws", () => { const events = new RuntimeEvents(); const throwing = vi.fn(() => { throw new Error("once failed"); }); const after = vi.fn(); events.once("runtime:ready", throwing); events.on("runtime:ready", after); events.emit("runtime:ready", { startedAt: 1 }); events.emit("runtime:ready", { startedAt: 2 }); expect(throwing).toHaveBeenCalledTimes(1); expect(after).toHaveBeenCalledTimes(2); }); it("supports onAny tap without affecting event listeners", () => { const events = new RuntimeEvents(); const tap = vi.fn(); const handler = vi.fn(); const offTap = events.onAny(tap); events.on("scene:changed", handler); events.emit("scene:changed", { current: "a", previous: undefined }); offTap(); events.emit("scene:changed", { current: "b", previous: "a" }); expect(tap).toHaveBeenCalledTimes(1); expect(tap).toHaveBeenCalledWith("scene:changed", { current: "a", previous: undefined }); expect(handler).toHaveBeenCalledTimes(2); }); }); describe("AppRuntime plugin lifecycle", () => { it("installs plugin and keeps compatibility exports", () => { const runtime = new AppRuntime(); const setup = vi.fn(); const plugin: RuntimePlugin = { name: "test-plugin", setup, }; runtime.use(plugin); runtime.use(plugin); expect(setup).toHaveBeenCalledTimes(1); expect(runtime.game).toBeDefined(); expect(runtime.sceneManager).toBeDefined(); expect(runtime.events).toBeDefined(); expect(runtime.assetManager).toBeDefined(); expect(runtime.nodes).toBeDefined(); }); });