v4: the playlists page is reachable at every width, and lists the user's own — below 1150px the header's text link was hidden and nothing replaced it: the avatar menu's "My playlists" led to the user's own, and a guest has no menu. The menu's entry is now "Playlists", to /playlists, at every width, and the page gains a tab after its orders, "Mine", for a signed-in user: their own playlists, private ones too, from GET /api/users/:id/playlists in the default order, narrowed by the category menu and kept live as the others are. It is /playlists?mine=1 (PLAYLISTS_TABS, parsePlaylistsTab, shared/paths.ts), a tab taking the order's place; a guest isn't offered it, and its link sends one to sign in. PlaylistSortTabs takes the tabs it shows, a profile's being the orders alone. The text link shows to a guest at every width, their header giving up its margins under 400px rather than the link, and to a signed-in user where the row has room beside the icons, as measured in every style and both languages: from 560px up to 860px, where the index page's tabs move into the header, and from 1150px as before. A guest in French in the kanagawa style still overflows the header under 340px, by up to 19px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,13 +22,10 @@ describe("UserMenu", () => {
|
||||
"href",
|
||||
"/@ann",
|
||||
);
|
||||
// One playlists link, the user's own: the header's leads to everyone's.
|
||||
expect(screen.getAllByRole("menuitem", { name: /playlists/i }))
|
||||
.toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(screen.getByRole("menuitem", { name: "My playlists" }))
|
||||
.toHaveAttribute("href", "/@ann/playlists");
|
||||
// One playlists link, to the page of everyone's, whose last tab is the
|
||||
// user's own: the header's text link is hidden where it has no room.
|
||||
expect(screen.getByRole("menuitem", { name: "Playlists" }))
|
||||
.toHaveAttribute("href", "/playlists");
|
||||
expect(screen.getByRole("menuitem", { name: "Community" })).toHaveAttribute(
|
||||
"href",
|
||||
"/community",
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
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, page, wire } from "../fixtures.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";
|
||||
|
||||
@@ -15,12 +23,14 @@ const mixes = makeCategory({
|
||||
});
|
||||
const music = makeCategory({ id: "dc1", slug: "music", name: "Music" });
|
||||
|
||||
function open(url: string) {
|
||||
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 };
|
||||
}
|
||||
@@ -140,6 +150,77 @@ describe("Playlists", () => {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user