Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions webapp/e2e/studio-nocturne.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions webapp/src/components/song-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();
const rowIds = songs.map((song) => {
const copy = seen.get(song.id) ?? 0;
seen.set(song.id, copy + 1);
return `${song.id}#${copy}`;
});

return (
<div className="songs-scroll">
<table className="songs">
Expand All @@ -131,7 +140,10 @@ export function SongTable({
const starred = stars[song.id] ?? song.starred_at !== null;
const active = player.current?.id === song.id;
return (
<tr key={song.id} className={active ? "active" : undefined}>
<tr
key={rowIds[position]}
className={active ? "active" : undefined}
>
<td className="index">
{numbering === "track"
? (song.track ?? position + 1)
Expand Down Expand Up @@ -160,7 +172,7 @@ export function SongTable({
<div>
<StarRating
label={`${t("rating.label")}: ${song.title}`}
name={`rating-${song.id}`}
name={`rating-${rowIds[position]}`}
value={ratings[song.id] ?? song.user_rating ?? 0}
onRate={(rating) => void rate(song, rating)}
/>
Expand Down
Loading