fix(playlists): an Undo puts a row back after its old neighbour, not at a stale index

A raw index recorded at removal drifted once anything changed before the Undo.
Both lists now read the list fresh and re-insert each row after the row that
preceded it (lib/reinsert). A failed watched toggle in a Plex playlist also
puts the row's state back: its local copy was never re-seeded.
This commit is contained in:
2026-09-27 14:02:15 +02:00
parent b456232d00
commit de62035889
9 changed files with 225 additions and 32 deletions
@@ -152,14 +152,22 @@ describe("PlexPlaylistView · a row's menu", () => {
it("removes a title with no confirm, and the toast's Undo puts it back at its index", async () => {
mount([movie("1"), movie("2"), movie("3")]);
await screen.findByText("Movie 2");
vi.spyOn(api, "plexPlaylistRemoveItem").mockResolvedValue({} as never);
vi.spyOn(api, "plexPlaylistRemoveItem").mockImplementation(async () => {
// The server's side of it: the Undo reads the list fresh, and the row is gone from it.
vi.mocked(api.plexPlaylist).mockResolvedValue({
id: 5,
title: "Films",
items: [movie("1"), movie("3")],
} as never);
return {} as never;
});
const add = vi.spyOn(api, "plexPlaylistAddBulk").mockResolvedValue({} as never);
await pick(() => fireEvent.contextMenu(rowOf("Movie 2")), "plex.playlist.removeFromPlaylist");
await waitFor(() => expect(api.plexPlaylistRemoveItem).toHaveBeenCalledWith(5, "2"));
expect(h.confirm).not.toHaveBeenCalled();
await waitFor(() => expect(toast()?.message).toBe("plex.playlist.removedToast:Movie 2"));
act(() => toast()!.action!.onClick());
expect(add).toHaveBeenCalledWith(5, ["2"], [1]);
await waitFor(() => expect(add).toHaveBeenCalledWith(5, ["2"], [1]));
});
it("plays and removes a whole show from its group's menu", async () => {
@@ -313,3 +321,24 @@ describe("PlexPlaylistView · the header's '…'", () => {
);
});
});
// E28 review E2: the watched toggle flips the view's LOCAL copy of the rows. On a failure the
// resync refetches an identical list, react-query keeps the same `data`, and the copy was never
// re-seeded — the row stayed "watched" on screen while Plex still had it unwatched.
describe("PlexPlaylistView · a failed watched toggle", () => {
it("puts the row's watched state back", async () => {
mount([movie("1"), movie("2")]);
await screen.findByText("Movie 2");
vi.spyOn(api, "plexSetState").mockRejectedValue(new Error("offline"));
await pick(() => fireEvent.contextMenu(rowOf("Movie 2")), "plex.markWatched");
await waitFor(() =>
expect(h.notify).toHaveBeenCalledWith(
expect.objectContaining({ level: "error", title: "plex.watchedFailed" }),
),
);
await act(async () => {});
fireEvent.keyDown(document.activeElement ?? document.body, { key: "Escape" });
await openMenu(() => fireEvent.contextMenu(rowOf("Movie 2")));
expect(menuRows()).toContain("plex.markWatched");
});
});