import { describe, expect, test } from "vitest"; import { Route } from "react-router"; import { screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Playlists } from "../../pages/Playlists.tsx"; import { calls, route } from "../fetch.ts"; import { makeCategory, makePlaylist, makeSession, page, wire, } from "../fixtures.ts"; import type { Session } from "../../utils/auth/storage.ts"; import { currentPath, renderApp } from "../render.tsx"; import { FakeWebSocket } from "../ws.ts"; const mixes = makeCategory({ id: "pc1", kind: "playlist", slug: "mixes", name: "Mixes", }); const music = makeCategory({ id: "dc1", slug: "music", name: "Music" }); function open(url: string, session?: Session) { const user = userEvent.setup(); renderApp(, { route: url, path: "/playlists/:categorySlug?", categories: [music, mixes], session, extraRoutes: , }); return { user, socket: FakeWebSocket.last }; } describe("Playlists", () => { test("every public playlist; the menu offers the playlist categories only", async () => { route( "GET /api/playlists", () => page([makePlaylist({ title: "Summer", categoryIds: ["pc1"] })]), ); const { user } = open("/playlists"); expect(await screen.findByText("Summer")).toBeInTheDocument(); expect(calls("GET /api/playlists")[0].url.searchParams.get("category")) .toBeNull(); // Its chip leads to its category's playlists. expect(screen.getByRole("link", { name: "Mixes" })).toHaveAttribute( "href", "/playlists/mixes", ); await user.click(screen.getByRole("button", { name: "All" })); expect(screen.getByRole("option", { name: "Mixes" })).toBeInTheDocument(); expect(screen.queryByRole("option", { name: "Music" })).toBeNull(); await user.click(screen.getByRole("option", { name: "Mixes" })); expect(currentPath()).toBe("/playlists/mixes"); }); test("a category's playlists, kept to it as the socket brings more", async () => { route( "GET /api/playlists", () => page([ makePlaylist({ id: "p1", title: "Filed", categoryIds: ["pc1"] }), ]), ); const { socket } = open("/playlists/mixes"); expect(await screen.findByText("Filed")).toBeInTheDocument(); expect(calls("GET /api/playlists")[0].url.searchParams.get("category")) .toBe("mixes"); socket.welcome(); socket.receive({ type: "playlist_created", playlist: wire(makePlaylist({ title: "Elsewhere", categoryIds: [] })), }); socket.receive({ type: "playlist_created", playlist: wire( makePlaylist({ title: "Also filed", categoryIds: ["pc1"] }), ), }); expect(screen.getByText("Also filed")).toBeInTheDocument(); expect(screen.queryByText("Elsewhere")).toBeNull(); // Taken out of the category, it leaves. socket.receive({ type: "playlist_updated", playlist: wire( makePlaylist({ id: "p1", title: "Filed", categoryIds: [] }), ), }); expect(screen.queryByText("Filed")).toBeNull(); }); test("the tabs change the order, in the URL, which picking a category keeps", async () => { route( "GET /api/playlists", (req) => page([ makePlaylist({ title: `By ${req.url.searchParams.get("sort") ?? "default"}`, }), ]), ); const { user } = open("/playlists"); expect(await screen.findByText("By default")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Most dumps" })); expect(currentPath()).toBe("/playlists?sort=dumps"); expect(await screen.findByText("By dumps")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "All" })); await user.click(screen.getByRole("option", { name: "Mixes" })); expect(currentPath()).toBe("/playlists/mixes?sort=dumps"); const asked = calls("GET /api/playlists").at(-1)!.url.searchParams; expect([asked.get("category"), asked.get("sort")]).toEqual([ "mixes", "dumps", ]); // The default order is the bare path, and so is one the URL misnames. await user.click(screen.getByRole("button", { name: "Updated" })); expect(currentPath()).toBe("/playlists/mixes"); }); test("a playlist that takes a dump goes first in the updated order", async () => { const at = (id: string, day: number) => makePlaylist({ id, title: `Playlist ${id}`, dumpCount: 1, lastDumpAt: new Date(Date.UTC(2026, 0, day)), }); route("GET /api/playlists", () => page([at("a", 20), at("b", 10)])); const { socket } = open("/playlists"); await screen.findByText("Playlist a"); socket.welcome(); socket.receive({ type: "playlist_dumps_updated", playlistId: "b", dumpIds: ["x", "y"], lastDumpAt: "2026-01-25T00:00:00.000Z", }); expect( screen.getAllByRole("link", { name: /^Playlist \w$/ }).map((a) => a.textContent ), ).toEqual(["Playlist b", "Playlist a"]); expect(screen.getByText("2 dumps")).toBeInTheDocument(); }); test("a signed-in user's last tab lists their own, private ones too", async () => { const session = makeSession({ id: "ann-id", username: "ann" }); route( "GET /api/playlists", () => page([makePlaylist({ title: "Theirs" })]), ); route( "GET /api/users/:u/playlists", () => page([ makePlaylist({ title: "Hidden", userId: "ann-id", isPublic: false }), ]), ); const { user, socket } = open("/playlists/mixes?sort=dumps", session); expect(await screen.findByText("Theirs")).toBeInTheDocument(); // Their own come in one order: the tab takes the order's place. await user.click(screen.getByRole("button", { name: "Mine" })); expect(currentPath()).toBe("/playlists/mixes?mine=1"); expect(await screen.findByText("Hidden")).toBeInTheDocument(); expect(screen.queryByText("Theirs")).toBeNull(); const asked = calls("GET /api/users/:u/playlists").at(-1)!.url; expect(asked.pathname).toBe("/api/users/ann-id/playlists"); expect([asked.searchParams.get("category"), asked.searchParams.get("sort")]) .toEqual(["mixes", null]); // The socket brings their new ones, private or not, and no one else's. socket.welcome(); for ( const playlist of [ makePlaylist({ title: "Mine too", userId: "ann-id", isPublic: false, categoryIds: ["pc1"], }), makePlaylist({ title: "Bob's", userId: "bob-id", categoryIds: ["pc1"], }), ] ) { socket.receive({ type: "playlist_created", playlist: wire(playlist) }); } expect(screen.getByText("Mine too")).toBeInTheDocument(); expect(screen.queryByText("Bob's")).toBeNull(); // Picking a category keeps the tab, an order leaves it. await user.click(screen.getByRole("button", { name: "Mixes" })); await user.click(screen.getByRole("option", { name: "All" })); expect(currentPath()).toBe("/playlists?mine=1"); await user.click(screen.getByRole("button", { name: "New" })); expect(currentPath()).toBe("/playlists?sort=new"); }); test("a guest has no such tab", async () => { route( "GET /api/playlists", () => page([makePlaylist({ title: "Theirs" })]), ); open("/playlists"); expect(await screen.findByText("Theirs")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Mine" })).toBeNull(); }); test("a guest sent to their own playlists signs in first", () => { open("/playlists?mine=1"); expect(currentPath()).toBe("/login?next=%2Fplaylists%3Fmine%3D1"); }); test("a slug that names no playlist category is not found", async () => { route("GET /api/playlists", () => page([])); open("/playlists/music"); expect(await screen.findByText("This page does not exist.")) .toBeInTheDocument(); }); });