feat(sp6): Inbox page + useInboxLanes hook

This commit is contained in:
Tyler
2026-06-20 18:41:44 -06:00
parent 610e580844
commit b607e4c459
4 changed files with 162 additions and 6 deletions
+45
View File
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, renderHook, waitFor } from "@testing-library/react";
import { useInboxLanes } from "./useInboxLanes";
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.useRealTimers();
});
describe("useInboxLanes", () => {
it("loads lanes on mount", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
rejected: [],
candidates: [],
unmatched: [],
done_today: [],
}),
}),
);
const { result } = renderHook(() => useInboxLanes());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.lanes).toEqual({
rejected: [],
candidates: [],
unmatched: [],
done_today: [],
});
});
it("exposes an error when fetch fails", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: false, status: 500, json: async () => ({}) }),
);
const { result } = renderHook(() => useInboxLanes());
await waitFor(() => expect(result.current.error).not.toBeNull());
expect(result.current.error?.message).toMatch(/500/);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchInboxLanes, type InboxLanes } from "@/lib/inbox-api";
/** Default poll interval. Tail-integration (T19) will replace this. */
const POLL_INTERVAL_MS = 5_000;
export function useInboxLanes() {
const [lanes, setLanes] = useState<InboxLanes>({
rejected: [],
candidates: [],
unmatched: [],
done_today: [],
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const refetch = useCallback(async () => {
try {
const next = await fetchInboxLanes();
setLanes(next);
setError(null);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refetch();
pollRef.current = setInterval(() => {
void refetch();
}, POLL_INTERVAL_MS);
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, [refetch]);
return { lanes, loading, error, refetch };
}