test(web): cover timeline sse edge paths
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { SSE_EVENT_TYPES } from "@capakraken/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const invalidateQueries = vi.fn();
|
||||
const effectCleanups: Array<() => void> = [];
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual<typeof import("react")>("react");
|
||||
return {
|
||||
...actual,
|
||||
useEffect: (callback: () => void | (() => void)) => {
|
||||
const cleanup = callback();
|
||||
if (typeof cleanup === "function") {
|
||||
effectCleanups.push(cleanup);
|
||||
}
|
||||
},
|
||||
useRef: <T,>(value: T) => ({ current: value }),
|
||||
};
|
||||
});
|
||||
|
||||
import { useTimelineSSE } from "./useTimelineSSE.js";
|
||||
|
||||
class MockEventSource {
|
||||
static instances: MockEventSource[] = [];
|
||||
|
||||
onopen: (() => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
close = vi.fn();
|
||||
|
||||
constructor(public readonly url: string) {
|
||||
MockEventSource.instances.push(this);
|
||||
}
|
||||
|
||||
emitOpen() {
|
||||
this.onopen?.();
|
||||
}
|
||||
|
||||
emitMessage(data: string) {
|
||||
this.onmessage?.({ data } as MessageEvent);
|
||||
}
|
||||
|
||||
emitError() {
|
||||
this.onerror?.();
|
||||
}
|
||||
}
|
||||
|
||||
describe("useTimelineSSE", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
effectCleanups.length = 0;
|
||||
MockEventSource.instances.length = 0;
|
||||
vi.stubGlobal("EventSource", MockEventSource);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (effectCleanups.length > 0) {
|
||||
effectCleanups.pop()?.();
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("ignores malformed SSE payloads without invalidating queries", () => {
|
||||
useTimelineSSE();
|
||||
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
|
||||
MockEventSource.instances[0]?.emitMessage("{not-json");
|
||||
|
||||
expect(invalidateQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not invalidate queries for ping messages", () => {
|
||||
useTimelineSSE();
|
||||
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
|
||||
MockEventSource.instances[0]?.emitMessage(JSON.stringify({ type: SSE_EVENT_TYPES.PING }));
|
||||
|
||||
expect(invalidateQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resets reconnect backoff after ping and reconnects only once per pending timer", () => {
|
||||
useTimelineSSE();
|
||||
|
||||
const firstConnection = MockEventSource.instances[0];
|
||||
expect(firstConnection).toBeDefined();
|
||||
|
||||
firstConnection?.emitError();
|
||||
firstConnection?.emitError();
|
||||
expect(firstConnection?.close).toHaveBeenCalledTimes(2);
|
||||
|
||||
vi.advanceTimersByTime(1999);
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(MockEventSource.instances).toHaveLength(2);
|
||||
|
||||
const secondConnection = MockEventSource.instances[1];
|
||||
secondConnection?.emitMessage(JSON.stringify({ type: SSE_EVENT_TYPES.PING }));
|
||||
secondConnection?.emitError();
|
||||
|
||||
vi.advanceTimersByTime(1999);
|
||||
expect(MockEventSource.instances).toHaveLength(2);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(MockEventSource.instances).toHaveLength(3);
|
||||
|
||||
expect(invalidateQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears a pending reconnect when the hook is disposed", () => {
|
||||
useTimelineSSE();
|
||||
|
||||
const firstConnection = MockEventSource.instances[0];
|
||||
expect(firstConnection).toBeDefined();
|
||||
|
||||
firstConnection?.emitError();
|
||||
|
||||
while (effectCleanups.length > 0) {
|
||||
effectCleanups.pop()?.();
|
||||
}
|
||||
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
expect(firstConnection?.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user