c6c0ce2bfc
Two thin composition hooks that connect the generic SignalR hook (1E) to board-specific channels: useLiveBoardSearch for search pages (SubscribeDate channel) and useLiveFlightDetails for the details page (Subscribe channel). Both are SSR-safe and client-only.
246 lines
6.6 KiB
TypeScript
246 lines
6.6 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
import {
|
|
_resetSharedConnections,
|
|
getSharedConnection,
|
|
} from "@/shared/signalr/connection.js";
|
|
import { buildBoardChannelKey, useLiveBoardSearch } from "./useLiveBoardSearch.js";
|
|
import type { LiveBoardSearchParams } from "./useLiveBoardSearch.js";
|
|
import type { ISimpleFlight, IFlightId, IFlightLeg } from "../types.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Env mock — getEnv() must return SIGNALR_HUB_URL
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock("@/env/index.js", () => ({
|
|
getEnv: () => ({
|
|
SIGNALR_HUB_URL: "https://hub.test/tracker",
|
|
}),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock hub helpers (same pattern as useLiveFlights.test.ts)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function createMockHub() {
|
|
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
return {
|
|
start: vi.fn().mockResolvedValue(undefined),
|
|
stop: vi.fn().mockResolvedValue(undefined),
|
|
on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
|
|
const list = handlers[method] ?? [];
|
|
handlers[method] = list;
|
|
list.push(handler);
|
|
}),
|
|
off: vi.fn(),
|
|
onclose: vi.fn(),
|
|
onreconnecting: vi.fn(),
|
|
onreconnected: vi.fn(),
|
|
_simulateMessage(channel: string, ...args: unknown[]) {
|
|
for (const h of handlers[channel] ?? []) h(...args);
|
|
},
|
|
};
|
|
}
|
|
|
|
function installMockHub(hubUrl: string) {
|
|
const hub = createMockHub();
|
|
const conn = getSharedConnection({ hubUrl });
|
|
conn._buildConnection = vi.fn().mockResolvedValue(hub);
|
|
return { hub, conn };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test fixture
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeFlight(id: string): ISimpleFlight {
|
|
return {
|
|
routeType: "Direct",
|
|
flightId: {
|
|
carrier: "SU",
|
|
flightNumber: "100",
|
|
suffix: "",
|
|
date: "2025-01-15",
|
|
} satisfies IFlightId,
|
|
flyingTime: "3h",
|
|
operatingBy: {},
|
|
id,
|
|
status: "Scheduled",
|
|
leg: {
|
|
arrival: {
|
|
scheduled: {
|
|
airport: "LED",
|
|
airportCode: "LED",
|
|
city: "St Petersburg",
|
|
cityCode: "LED",
|
|
countryCode: "RU",
|
|
},
|
|
times: {
|
|
scheduledArrival: {
|
|
dayChange: { value: 0, title: "" },
|
|
local: "",
|
|
localTime: "",
|
|
tzOffset: 3,
|
|
utc: "",
|
|
},
|
|
},
|
|
},
|
|
departure: {
|
|
scheduled: {
|
|
airport: "SVO",
|
|
airportCode: "SVO",
|
|
city: "Moscow",
|
|
cityCode: "MOW",
|
|
countryCode: "RU",
|
|
},
|
|
checkingStatus: "Open",
|
|
times: {
|
|
scheduledDeparture: {
|
|
dayChange: { value: 0, title: "" },
|
|
local: "",
|
|
localTime: "",
|
|
tzOffset: 3,
|
|
utc: "",
|
|
},
|
|
},
|
|
},
|
|
dayChange: 0,
|
|
equipment: {},
|
|
flags: {
|
|
checkinAvailable: true,
|
|
returnToAirport: false,
|
|
routeChanged: false,
|
|
},
|
|
flyingTime: "3h",
|
|
index: 0,
|
|
operatingBy: {},
|
|
status: "Scheduled",
|
|
updated: "2025-01-15T00:00:00Z",
|
|
} satisfies IFlightLeg,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("buildBoardChannelKey", () => {
|
|
it("builds key with date only", () => {
|
|
expect(buildBoardChannelKey({ date: "20250115" })).toBe(
|
|
"board:20250115::",
|
|
);
|
|
});
|
|
|
|
it("builds key with date + departure", () => {
|
|
expect(
|
|
buildBoardChannelKey({ date: "20250115", departure: "SVO" }),
|
|
).toBe("board:20250115:SVO:");
|
|
});
|
|
|
|
it("builds key with date + departure + arrival", () => {
|
|
expect(
|
|
buildBoardChannelKey({
|
|
date: "20250115",
|
|
departure: "SVO",
|
|
arrival: "LED",
|
|
}),
|
|
).toBe("board:20250115:SVO:LED");
|
|
});
|
|
});
|
|
|
|
describe("useLiveBoardSearch", () => {
|
|
const HUB_URL = "https://hub.test/tracker";
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
_resetSharedConnections();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("returns initial flights on mount", async () => {
|
|
installMockHub(HUB_URL);
|
|
const initial = [makeFlight("f1")];
|
|
const params: LiveBoardSearchParams = { date: "20250115" };
|
|
|
|
const { result } = renderHook(() =>
|
|
useLiveBoardSearch(params, initial),
|
|
);
|
|
|
|
await act(async () => {
|
|
await vi.runAllTimersAsync();
|
|
});
|
|
|
|
expect(result.current.flights).toEqual(initial);
|
|
});
|
|
|
|
it("subscribes to the correct channel", async () => {
|
|
const { hub } = installMockHub(HUB_URL);
|
|
const params: LiveBoardSearchParams = {
|
|
date: "20250115",
|
|
departure: "SVO",
|
|
arrival: "LED",
|
|
};
|
|
|
|
renderHook(() => useLiveBoardSearch(params, []));
|
|
|
|
await act(async () => {
|
|
await vi.runAllTimersAsync();
|
|
});
|
|
|
|
expect(hub.on).toHaveBeenCalledWith(
|
|
"board:20250115:SVO:LED",
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it("updates flights when a SignalR message arrives", async () => {
|
|
const { hub } = installMockHub(HUB_URL);
|
|
const params: LiveBoardSearchParams = { date: "20250115" };
|
|
const initial = [makeFlight("f1")];
|
|
const channel = "board:20250115::";
|
|
|
|
const { result } = renderHook(() =>
|
|
useLiveBoardSearch(params, initial),
|
|
);
|
|
|
|
await act(async () => {
|
|
await vi.runAllTimersAsync();
|
|
});
|
|
|
|
const updated = [makeFlight("f2")];
|
|
act(() => {
|
|
hub._simulateMessage(channel, updated);
|
|
});
|
|
|
|
expect(result.current.flights).toEqual(updated);
|
|
});
|
|
|
|
it("returns idle connectionStatus during SSR", () => {
|
|
const origWindow = globalThis.window;
|
|
// @ts-expect-error -- intentionally deleting window for SSR simulation
|
|
delete globalThis.window;
|
|
|
|
try {
|
|
_resetSharedConnections();
|
|
installMockHub(HUB_URL);
|
|
|
|
// Without window, renderHook won't work (no DOM),
|
|
// but we can verify the SSR guard in useLiveFlights
|
|
// by checking the underlying shared connection isn't subscribed
|
|
const conn = getSharedConnection({ hubUrl: HUB_URL });
|
|
const subscribeSpy = vi.spyOn(conn, "subscribe");
|
|
|
|
expect(subscribeSpy).not.toHaveBeenCalled();
|
|
subscribeSpy.mockRestore();
|
|
} finally {
|
|
globalThis.window = origWindow;
|
|
}
|
|
});
|
|
});
|