v4: what comes into a live list goes where its date puts it — a dump edited into a category stayed out of the index's feed of it until a reload, and a dump or a playlist made public went on top of the others' lists as if just posted. The index's feed now takes a dump edited into its scope, and orders the dumps that arrive by date, as the server pages them; a created playlist goes by date in the profile's and /playlists' lists, a new one first as before. Both use withByDate (utils/feed/pagedList.ts), which leaves an item older than every one loaded to a later page. The search runs again after a reconnection, for every page of dumps loaded, with its users and playlists; it shares refetchPages with usePagedList's resync. injectDump's doc said a private dump is never broadcast, while its poster gets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 07:43:28 +00:00
parent 308a8299f3
commit 7c00db2b84
11 changed files with 278 additions and 61 deletions

View File

@@ -264,6 +264,44 @@ describe("Index feeds", () => {
expect(screen.queryByText("A song")).not.toBeInTheDocument();
});
test("a dump edited into the category comes in where its date puts it", async () => {
const at = (minutes: number) => new Date(Date.now() - minutes * 60_000);
const tagged = (id: string, title: string, minutes: number) =>
makeDump({ id, title, categoryIds: ["c-music"], createdAt: at(minutes) });
route(
"GET /api/dumps",
() => page([tagged("n", "Newer", 1), tagged("o", "Older", 30)]),
);
const { socket } = setup("/music/new");
await screen.findByText("Older");
socket.receive({
type: "dump_updated",
dump: wire(tagged("m", "Now music", 10)),
});
expect(titles()).toEqual(["Newer", "Now music", "Older"]);
});
test("a dump made public long after it was posted goes by its date, not on top", async () => {
const at = (minutes: number) => new Date(Date.now() - minutes * 60_000);
route("GET /api/dumps", () =>
page([
makeDump({ id: "n", title: "Newer", createdAt: at(1) }),
makeDump({ id: "o", title: "Older", createdAt: at(30) }),
]));
const { socket } = setup("/new");
await screen.findByText("Older");
// To everyone but its poster, it arrives as created.
socket.receive({
type: "dump_created",
dump: wire(makeDump({ id: "m", title: "Unveiled", createdAt: at(10) })),
});
socket.receive({
type: "dump_created",
dump: wire(makeDump({ id: "a", title: "Ancient", createdAt: at(600) })),
});
expect(titles()).toEqual(["Newer", "Unveiled", "Older", "Ancient"]);
});
test("an unknown category is a 404", async () => {
route("GET /api/dumps", () => fail("nope", 404));
setup("/nonsense/new");

View File

@@ -238,4 +238,50 @@ describe("Search", () => {
await user.click(screen.getByRole("button", { name: /Playlists/ }));
expect(screen.queryByText("Cats")).not.toBeInTheDocument();
});
test("a reconnect runs the search again, for every page loaded", async () => {
route(
"GET /api/search",
(req) =>
req.url.searchParams.get("page") === "2"
? results({ dumps: [makeDump({ title: "Second page" })] })
: results({
dumps: [makeDump({ title: "First page" })],
hasMore: true,
users: [makeUser({ username: "catlover" })],
}),
);
open("/search?q=cat");
const socket = FakeWebSocket.last;
socket.welcome();
await screen.findByText("First page");
const sentinel = FakeIntersectionObserver.instances.findLast((o) =>
o.elements.size > 0
)!;
act(() => sentinel.report(true));
await screen.findByText("Second page");
// Gone while the socket was down: an edit, a deletion, a new user.
route(
"GET /api/search",
(req) =>
req.url.searchParams.get("page") === "2"
? results({ dumps: [] })
: results({
dumps: [makeDump({ title: "First page, edited" })],
hasMore: true,
users: [
makeUser({ username: "catlover" }),
makeUser({ username: "cat2" }),
],
}),
);
socket.drop();
await act(() => new Promise((r) => setTimeout(r, 600)));
FakeWebSocket.last.welcome();
expect(await screen.findByText("First page, edited")).toBeInTheDocument();
expect(screen.queryByText("Second page")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Users (2)" }))
.toBeInTheDocument();
});
});