Files
gerbeur/src/test/pages/Playlists.test.tsx
2026-10-10 09:14:55 +00:00

231 lines
7.9 KiB
TypeScript

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(<Playlists />, {
route: url,
path: "/playlists/:categorySlug?",
categories: [music, mixes],
session,
extraRoutes: <Route path="/login" element={null} />,
});
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();
});
});