import { describe, expect, test } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; import { type ReactNode, useState } from "react"; import { MemoryRouter } from "react-router"; import { AuthContext } from "../../../contexts/auth/AuthContext.ts"; import { useUserDumpFeed } from "../../../hooks/dump/useUserDumpFeed.ts"; import { calls, fail, route } from "../../fetch.ts"; import { makeDump, makeSession, makeUser, page, wire } from "../../fixtures.ts"; import { FakeIntersectionObserver } from "../../setup.ts"; import type { Session } from "../../../utils/auth/storage.ts"; describe("useUserDumpFeed", () => { const ann = makeUser({ username: "ann" }); function setup( opts: { session?: ReturnType | null } = {}, ) { const session = opts.session ?? null; function Wrapper({ children }: { children: ReactNode }) { const [authResponse, setAuthResponse] = useState(session); return ( {children} ); } return renderHook( ({ username }: { username?: string }) => useUserDumpFeed(username, "dumps", "cache-key"), { initialProps: { username: "ann" as string | undefined }, wrapper: Wrapper, }, ); } test("loads the profile and the first page", async () => { route("GET /api/users/:u", () => wire(ann)); route("GET /api/users/:u/dumps", () => page([makeDump({ id: "a" })], true)); const session = makeSession(); const { result } = setup({ session }); await waitFor(() => expect(result.current.state.status).toBe("loaded")); const s = result.current.state as Extract< typeof result.current.state, { status: "loaded" } >; expect(s.profileUser.username).toBe("ann"); expect(s.items[0].createdAt).toBeInstanceOf(Date); expect(s.hasMore).toBe(true); const [req] = calls("GET /api/users/:u/dumps"); expect(req.url.searchParams.get("page")).toBe("1"); expect(req.headers.get("Authorization")).toBe(`Bearer ${session.token}`); }); test("an unknown user is an error", async () => { route("GET /api/users/:u", () => fail("User not found", 404)); route("GET /api/users/:u/dumps", () => page([])); const { result } = setup(); await waitFor(() => expect(result.current.state.status).toBe("error")); }); test("an offline load says so", async () => { route("GET /api/users/:u", () => new TypeError("Failed to fetch")); route("GET /api/users/:u/dumps", () => page([])); const { result } = setup(); await waitFor(() => expect(result.current.state).toMatchObject({ status: "error", error: expect.stringMatching(/Could not connect/), }) ); }); test("scrolling to the sentinel loads the next page", async () => { route("GET /api/users/:u", () => wire(ann)); route( "GET /api/users/:u/dumps", (req) => req.url.searchParams.get("page") === "1" ? page([makeDump({ id: "a" })], true) : page([makeDump({ id: "b" })], false), ); const { result } = setup(); await waitFor(() => expect(result.current.state.status).toBe("loaded")); const sentinel = document.createElement("div"); act(() => { result.current.sentinelRef(sentinel); }); const obs = FakeIntersectionObserver.instances.at(-1)!; act(() => obs.report(true)); await waitFor(() => { const s = result.current.state; expect(s.status === "loaded" && s.items.map((d) => d.id)).toEqual([ "a", "b", ]); }); const s = result.current.state as Extract< typeof result.current.state, { status: "loaded" } >; expect(s.hasMore).toBe(false); expect(s.page).toBe(2); }); test("changing user resets to loading", async () => { route( "GET /api/users/:u", (req) => wire(makeUser({ username: req.params.u })), ); route("GET /api/users/:u/dumps", () => page([])); const { result, rerender } = setup(); await waitFor(() => expect(result.current.state.status).toBe("loaded")); rerender({ username: "bob" }); expect(result.current.state.status).toBe("loading"); await waitFor(() => expect(result.current.state.status).toBe("loaded")); }); test("setItems edits the loaded list", async () => { route("GET /api/users/:u", () => wire(ann)); route( "GET /api/users/:u/dumps", () => page([makeDump({ id: "a" }), makeDump({ id: "b" })]), ); const { result } = setup(); await waitFor(() => expect(result.current.state.status).toBe("loaded")); act(() => result.current.setItems((prev) => prev.slice(1))); const s = result.current.state as Extract< typeof result.current.state, { status: "loaded" } >; expect(s.items.map((d) => d.id)).toEqual(["b"]); }); });