ResearchRAG / frontend /src /hooks /useSingleFlight.test.ts
riezqidr's picture
feat: implement useSingleFlight hook to prevent duplicate async actions and optimize request handling
c2d8746
Raw
History Blame Contribute Delete
2.68 kB
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useSingleFlight } from "./useSingleFlight";
/** A promise the test resolves by hand, so an action can be held open. */
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>((res) => {
resolve = res;
});
return { promise, resolve };
}
describe("useSingleFlight", () => {
it("refuses a second call while the first is still running", async () => {
const gate = deferred();
let started = 0;
const { result } = renderHook(() =>
useSingleFlight(async () => {
started += 1;
await gate.promise;
}),
);
await act(async () => {
void result.current.run();
void result.current.run();
void result.current.run();
});
expect(started).toBe(1);
await act(async () => {
gate.resolve();
});
// Latch released — the action is runnable again.
await act(async () => {
void result.current.run();
});
expect(started).toBe(2);
});
it("reports pending across the action's lifetime", async () => {
const gate = deferred();
const { result } = renderHook(() =>
useSingleFlight(async () => {
await gate.promise;
}),
);
expect(result.current.pending).toBe(false);
await act(async () => {
void result.current.run();
});
expect(result.current.pending).toBe(true);
await act(async () => {
gate.resolve();
});
expect(result.current.pending).toBe(false);
});
it("releases the latch when the action throws", async () => {
let started = 0;
const { result } = renderHook(() =>
useSingleFlight(async () => {
started += 1;
throw new Error("boom");
}),
);
// A rejected action must not wedge the control shut forever — that would
// turn one transient network error into a dead button for the session.
await act(async () => {
await result.current.run().catch(() => undefined);
});
await act(async () => {
await result.current.run().catch(() => undefined);
});
expect(started).toBe(2);
expect(result.current.pending).toBe(false);
});
it("passes arguments through to the action", async () => {
const seen: string[] = [];
const { result } = renderHook(() =>
useSingleFlight(async (title: string) => {
seen.push(title);
}),
);
await act(async () => {
await result.current.run("paper-a");
});
await act(async () => {
await result.current.run("paper-b");
});
expect(seen).toEqual(["paper-a", "paper-b"]);
});
});