import { describe, expect, test } from "vitest"; import { act, renderHook } from "@testing-library/react"; import { type ReactNode, useState } from "react"; import { defaultWSValue, WSContext, type WSContextValue, } from "../../../contexts/ws/WSContext.ts"; import type { Dump } from "../../../../shared/dump.ts"; import type { Playlist } from "../../../../shared/playlist.ts"; import { useDumpListSync } from "../../../hooks/dump/useDumpListSync.ts"; import { usePositionAwareSync } from "../../../hooks/feed/usePositionAwareSync.ts"; import { useWS } from "../../../hooks/ws/useWS.ts"; import { makeDump, makePlaylist } from "../../fixtures.ts"; /** A WS context whose value a test can push new events into. */ function liveWS() { let push: (patch: Partial) => void = () => {}; const base = defaultWSValue; function Wrapper({ children }: { children: ReactNode }) { const [value, setValue] = useState(base); push = (patch) => setValue((v) => ({ ...v, ...patch })); return {children}; } return { Wrapper, emit: (patch: Partial) => act(() => push(patch)), }; } function useList(initial: T[]) { const [items, setItems] = useState(initial); return { items, setItems }; } const ids = (xs: { id: string }[]) => xs.map((x) => x.id); describe("usePositionAwareSync", () => { test("a dump that goes private and back returns to its old place", () => { const ws = liveWS(); const initial = ["a", "b", "c"].map((id) => makeDump({ id, userId: "me" })); // Wire the pair exactly as UserPublicProfile does. const both = renderHook(() => { const { items, setItems } = useList(initial); const [event, setEvent] = useState(null); usePositionAwareSync(items, setItems, event, (d) => !d.isPrivate); useDumpListSync(setItems); return { items, setEvent }; }, { wrapper: ws.Wrapper }); const b = initial[1]; act(() => both.result.current.setEvent({ ...b, isPrivate: true })); ws.emit({ lastDumpEvent: { ...b, isPrivate: true } }); expect(ids(both.result.current.items)).toEqual(["a", "c"]); act(() => both.result.current.setEvent({ ...b, isPrivate: false })); ws.emit({ lastDumpEvent: { ...b, isPrivate: false } }); expect(ids(both.result.current.items)).toEqual(["a", "b", "c"]); }); test("a dump that goes private for others, as a deletion, comes back where it was", () => { const ws = liveWS(); const initial = ["a", "b", "c"].map((id) => makeDump({ id, userId: "me" })); const both = renderHook(() => { const { items, setItems } = useList(initial); const { lastDumpEvent } = useWS(); usePositionAwareSync(items, setItems, lastDumpEvent, (d) => !d.isPrivate); useDumpListSync(setItems); return { items }; }, { wrapper: ws.Wrapper }); ws.emit({ deletedDumpIds: new Set(["b"]) }); expect(ids(both.result.current.items)).toEqual(["a", "c"]); ws.emit({ deletedDumpIds: new Set(), lastDumpEvent: initial[1] }); expect(ids(both.result.current.items)).toEqual(["a", "b", "c"]); }); test("a position is only used once, and clamps to the list end", () => { const initial = ["a", "b", "c"].map((id) => makePlaylist({ id })); const hook = renderHook(() => { const { items, setItems } = useList(initial); const [event, setEvent] = useState(null); usePositionAwareSync(items, setItems, event, (p) => p.isPublic); return { items, setItems, setEvent }; }); const c = initial[2]; // It leaves, whatever takes it out, then the list shrinks further. act(() => hook.result.current.setItems((prev) => prev.slice(0, 2))); act(() => hook.result.current.setItems((prev) => prev.slice(0, 1))); act(() => hook.result.current.setEvent({ ...c, isPublic: true })); expect(ids(hook.result.current.items)).toEqual(["a", "c"]); // Used once: the same event again adds nothing. act(() => hook.result.current.setEvent({ ...c, isPublic: true, title: "x" }) ); expect(ids(hook.result.current.items)).toEqual(["a", "c"]); }); });