231 lines
7.9 KiB
TypeScript
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();
|
|
});
|
|
});
|