Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/stream/useBoardLiveUpdates.test.ts
T

99 lines
3.0 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PROJECT_BOARD_SSE_EVENT } from "~/utils/integrations/projects/projectBoardEvents.shared";
import { useBoardLiveUpdates } from "./useBoardLiveUpdates";
const mocks = vi.hoisted(() => ({
revalidate: vi.fn(),
state: "idle" as "idle" | "loading",
}));
vi.mock("react-router", () => ({
useRevalidator: () => ({
revalidate: mocks.revalidate,
state: mocks.state,
}),
}));
class FakeEventSource {
static instances: FakeEventSource[] = [];
readonly listeners = new Map<string, Set<EventListenerOrEventListenerObject>>();
closed = false;
constructor(readonly url: string) {
FakeEventSource.instances.push(this);
}
addEventListener(type: string, listener: EventListenerOrEventListenerObject) {
const listeners = this.listeners.get(type) ?? new Set();
listeners.add(listener);
this.listeners.set(type, listeners);
}
removeEventListener(type: string, listener: EventListenerOrEventListenerObject) {
this.listeners.get(type)?.delete(listener);
}
emit(type: string) {
const event = new Event(type);
for (const listener of this.listeners.get(type) ?? []) {
if (typeof listener === "function") listener(event);
else listener.handleEvent(event);
}
}
close() {
this.closed = true;
}
}
describe("useBoardLiveUpdates", () => {
beforeEach(() => {
mocks.revalidate.mockClear();
mocks.state = "idle";
FakeEventSource.instances = [];
vi.stubGlobal("EventSource", FakeEventSource);
});
afterEach(() => vi.unstubAllGlobals());
it("revalidates an idle board after a real change event", () => {
renderHook(() => useBoardLiveUpdates("/acme/projects/stream"));
expect(FakeEventSource.instances).toHaveLength(1);
act(() => FakeEventSource.instances[0]!.emit(PROJECT_BOARD_SSE_EVENT));
expect(mocks.revalidate).toHaveBeenCalledOnce();
});
it("does not duplicate route work while a navigation is active", () => {
const { rerender } = renderHook(
({ paused }) => useBoardLiveUpdates("/acme/projects/stream", { paused }),
{ initialProps: { paused: false } },
);
const source = FakeEventSource.instances[0]!;
rerender({ paused: true });
act(() => source.emit(PROJECT_BOARD_SSE_EVENT));
expect(mocks.revalidate).not.toHaveBeenCalled();
expect(FakeEventSource.instances).toHaveLength(1);
rerender({ paused: false });
act(() => source.emit(PROJECT_BOARD_SSE_EVENT));
expect(mocks.revalidate).toHaveBeenCalledOnce();
});
it("ignores an event while revalidation is busy and closes cleanly", () => {
mocks.state = "loading";
const { unmount } = renderHook(() => useBoardLiveUpdates("/acme/projects/stream"));
const source = FakeEventSource.instances[0]!;
act(() => source.emit(PROJECT_BOARD_SSE_EVENT));
expect(mocks.revalidate).not.toHaveBeenCalled();
unmount();
expect(source.closed).toBe(true);
expect(source.listeners.get(PROJECT_BOARD_SSE_EVENT)?.size).toBe(0);
});
});