You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

129 lines
3.8 KiB

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();
});
});