diff --git a/webapp/e2e/studio-nocturne.spec.ts b/webapp/e2e/studio-nocturne.spec.ts index 636a5e3..55368bf 100644 --- a/webapp/e2e/studio-nocturne.spec.ts +++ b/webapp/e2e/studio-nocturne.spec.ts @@ -1506,6 +1506,42 @@ test("saves a playlist as an .m3u8 the next player can read", async ({ expect(exportAsks).toEqual(["/api/v2/playlists/playlist-1/export"]); }); +/** + * A playlist holds a position, not a set: the same track can be on it twice. + * Each row's rating is a radio group named after the row, and a name built + * from the track alone put both copies in one group — the browser keeps one + * checked radio per group, so the first copy showed no rating at all. + */ +test("shows the same track twice in a playlist, each with its rating", async ({ + page, +}) => { + await page.route("**/api/v2/playlists", (route) => + route.fulfill({ + json: [ + { + ...playlists[0], + songs: [ + song(1, "Hidden Place", 5, true), + song(2, "Cocoon", 0, false), + song(1, "Hidden Place", 5, true), + ], + }, + ], + }), + ); + await page.goto("/playlists"); + await expect(page.getByRole("heading", { name: "Evening" })).toBeVisible(); + + const copies = page.getByRole("group", { name: "Rating: Hidden Place" }); + await expect(copies).toHaveCount(2); + await expect( + copies.first().getByRole("radio", { name: "5 stars" }), + ).toBeChecked(); + await expect( + copies.last().getByRole("radio", { name: "5 stars" }), + ).toBeChecked(); +}); + /** * An artist page carries what this account played, and nothing when it played * nothing. diff --git a/webapp/src/components/song-table.tsx b/webapp/src/components/song-table.tsx index 838b927..44fcc4f 100644 --- a/webapp/src/components/song-table.tsx +++ b/webapp/src/components/song-table.tsx @@ -123,6 +123,15 @@ export function SongTable({ } } + // A playlist can hold the same track twice, so the track id alone does not + // name a row: each copy is told apart by how many came before it. + const seen = new Map(); + const rowIds = songs.map((song) => { + const copy = seen.get(song.id) ?? 0; + seen.set(song.id, copy + 1); + return `${song.id}#${copy}`; + }); + return (
@@ -131,7 +140,10 @@ export function SongTable({ const starred = stars[song.id] ?? song.starred_at !== null; const active = player.current?.id === song.id; return ( - +
{numbering === "track" ? (song.track ?? position + 1) @@ -160,7 +172,7 @@ export function SongTable({
void rate(song, rating)} />