99 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
});
|