| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
- import { connectWs } from "../src/sync/ws.js";
- import type { WsSyncOptions } from "../src/sync/ws.js";
- import type { RenderSnapshot, ViewportHandle } from "../src/viewport.js";
- /** Minimal WebSocket stand-in with instance tracking for reconnect tests. */
- class FakeWebSocket {
- static instances: FakeWebSocket[] = [];
- static OPEN = 1;
- static CONNECTING = 0;
- readyState = FakeWebSocket.CONNECTING;
- onopen: (() => void) | null = null;
- onmessage: ((event: { data: string }) => void) | null = null;
- onclose: (() => void) | null = null;
- onerror: (() => void) | null = null;
- sent: string[] = [];
- constructor(public url: string) {
- FakeWebSocket.instances.push(this);
- }
- send(data: string): void {
- this.sent.push(data);
- }
- close(): void {
- this.readyState = 3; // CLOSED
- }
- open(): void {
- this.readyState = FakeWebSocket.OPEN;
- this.onopen?.();
- }
- receive(frame: unknown): void {
- this.onmessage?.({ data: JSON.stringify(frame) });
- }
- drop(): void {
- this.onclose?.();
- }
- }
- function fakeViewport(): ViewportHandle {
- return {
- render(_snapshot?: RenderSnapshot): void {},
- tileAt(): { x: number; y: number } | null {
- return null;
- },
- resize(): void {},
- destroy(): void {},
- };
- }
- function opts(overrides: Partial<WsSyncOptions> = {}): WsSyncOptions {
- return { url: "ws://127.0.0.1:1/mcp", viewport: fakeViewport(), ...overrides };
- }
- beforeEach(() => {
- vi.useFakeTimers();
- FakeWebSocket.instances = [];
- vi.stubGlobal("WebSocket", FakeWebSocket);
- });
- afterEach(() => {
- vi.unstubAllGlobals();
- vi.useRealTimers();
- });
- describe("connectWs sound forwarding (bug-fix round)", () => {
- it("forwards bgm-play and bgm-stop event frames to the sound player", () => {
- const calls: { event: string; payload: unknown }[] = [];
- const sound = { onEvent: (frame: { event: string; payload: unknown }) => calls.push(frame) } as never;
- connectWs(opts({ sound }));
- const socket = FakeWebSocket.instances[0]!;
- socket.open();
- socket.receive({ type: "event", event: "bgm-play", payload: { soundId: "bgm-spring", fadeMs: 1500 } });
- socket.receive({ type: "event", event: "bgm-stop", payload: { fadeMs: 800 } });
- expect(calls).toEqual([
- { type: "event", event: "bgm-play", payload: { soundId: "bgm-spring", fadeMs: 1500 } },
- { type: "event", event: "bgm-stop", payload: { fadeMs: 800 } },
- ]);
- });
- });
- describe("connectWs reconnect lifecycle (bug-fix round)", () => {
- it("destroy() cancels a queued reconnect — no socket leaks after teardown", () => {
- const handle = connectWs(opts());
- const first = FakeWebSocket.instances[0]!;
- first.open();
- first.drop(); // schedule reconnect
- expect(FakeWebSocket.instances).toHaveLength(1);
- handle.destroy();
- vi.advanceTimersByTime(600);
- expect(FakeWebSocket.instances).toHaveLength(1); // no second socket created
- });
- it("reconnects while alive and keeps working after a reconnect", () => {
- const handle = connectWs(opts());
- const first = FakeWebSocket.instances[0]!;
- first.open();
- first.drop();
- vi.advanceTimersByTime(500);
- expect(FakeWebSocket.instances).toHaveLength(2);
- const second = FakeWebSocket.instances[1]!;
- second.open();
- second.receive({ type: "event", event: "bgm-stop", payload: {} });
- handle.destroy();
- });
- });
|