v4: a playlist's editor has a URL, as a dump's has — it opened over the playlist's header as a state of the page, which a reload or a shared link lost, while a dump's editor is /@name/dumps/<slug>/edit. It is now /@name/playlists/<slug>/edit: Edit goes there, and Cancel or Save come back to the page in its place. Both paths render PlaylistDetail at the same place, so the playlist stays loaded, and a new slug keeps the view. Anyone who may not edit it gets the page, a guest the sign-in first. playlistPath takes the view, and parsePagePath reads it, so its link has no preview, as a dump editor's has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 22:22:24 +00:00
parent 21f4db268e
commit 9a215b0cc5
10 changed files with 130 additions and 46 deletions

View File

@@ -59,7 +59,15 @@ function open(
path: "/:handle/playlists/:slug",
session,
categories,
extraRoutes: <Route path="/" element={<p>home</p>} />,
extraRoutes: (
<>
<Route path="/" element={<p>home</p>} />
<Route
path="/:handle/playlists/:slug/edit"
element={<PlaylistDetail editing />}
/>
</>
),
});
const socket = FakeWebSocket.last;
socket.welcome();
@@ -142,12 +150,39 @@ describe("PlaylistDetail", () => {
await user.clear(box);
await user.type(box, "Better tunes");
await user.click(screen.getByRole("button", { name: "Save" }));
expect(await screen.findByText("Better tunes")).toBeInTheDocument();
// Saving closes the editor, whose text would be found first.
await waitFor(() => expect(currentPath()).toBe("/@owner/playlists/mix"));
expect(screen.getByText("Better tunes")).toBeInTheDocument();
expect(calls("PUT /api/playlists/:id")[0].body).toEqual({
description: "Better tunes",
isPublic: true,
});
});
test("the editor is a page: /edit opens it, closing goes back to the page", async () => {
const { user } = open({ url: "/@owner/playlists/mix/edit" });
expect(await screen.findByRole("textbox", { name: "Description" }))
.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(currentPath()).toBe("/@owner/playlists/mix");
await user.click(screen.getByRole("button", { name: "Edit" }));
expect(currentPath()).toBe("/@owner/playlists/mix/edit");
expect(calls("GET /api/users/:u/playlists/:slug")).toHaveLength(1);
});
test("at /edit, anyone else gets the page", async () => {
open({ session: visitor, url: "/@owner/playlists/mix/edit" });
await waitFor(() => expect(currentPath()).toBe("/@owner/playlists/mix"));
expect(screen.queryByRole("textbox", { name: "Description" })).toBeNull();
});
test("at /edit, a guest is asked to sign in first", async () => {
open({ session: null, url: "/@owner/playlists/mix/edit" });
await waitFor(() =>
expect(currentPath()).toBe(
"/login?next=%2F%40owner%2Fplaylists%2Fmix%2Fedit",
)
);
});
test("its categories show, and editing them sends the new set", async () => {
@@ -444,7 +479,7 @@ describe("PlaylistDetail", () => {
}),
);
expect(await screen.findByRole("alert")).toHaveTextContent("Forbidden");
expect(currentPath()).toBe("/@owner/playlists/mix");
expect(currentPath()).toBe("/@owner/playlists/mix/edit");
});
test("live: added dumps appear, removed ones fade, a deleted playlist sends you home", async () => {