fix(e24): the review's seven frontend defects on the Home surface

Shift+W was dead whenever anything was open. The global handler called `setPage` directly,
so the page change unmounted the player (it lives inside the module column) and that
unmount's reconcile reclaimed the entry the navigation had just pushed — the player closed
and the page snapped back. `closeThenNavigate` cannot serve here: a global key does not
know which overlay is open, or whether one is. `navigateOverOverlays` is the same fix one
step shorter — bury the live overlay entries first, so the unmount consumes the marks
instead of issuing `history.back()`.

`q.isError` had no `&& !data` guard, alone among 33 call sites and against the convention
`ui/QueryState` documents: react-query flips status to error on a failed REFETCH while
keeping the payload, and every mutation here invalidates the discover prefix, so one blip
while marking a card watched blanked a fully populated landing page.

The reroll's key rooted at "discover-pick", outside the prefix every mutation invalidates,
so a rerolled hero outlived the page: play it, finish it, and it was still the answer with
a zeroed progress bar. It now sits under the same root. A FAILED reroll also rewound the
hero to the card the page loaded with, several presses back, because a new key per press
means `keepPreviousData` (pending-only) cannot cover an error; the last resolved pick is
held instead.

The bookmark on a rail tile never updated — `VideoCard` reads `video.saved` from this
payload and keeps no optimistic state — so the star stayed empty and the next click
re-sent the same call.

The shelf reorder wrote a captured snapshot back on failure rather than re-asking the
server, which its own comment promised: two overlapping moves each reverted to their own
snapshot and settled on an order neither side held, and since `before` is the whole
payload a rollback also discarded any rail refresh that had landed meanwhile. It now
invalidates the discover PREFIX on settle, which also fixes the sibling budgets keeping
the pre-move order.

The hero/rail de-duplication matched one key, so for a Plex series — the rail row is the
show, the hero is the episode it resolves to — the same series printed twice; it now
matches both keys, as `hero.pick`'s own exclusion already did. And when the hero empties
the resume rail, the rail is hidden rather than announcing "nothing half-finished right
now" under a hero captioned "where you left off".

Ten regression tests, all mutation-checked. Three of them were rewritten after the first
mutation round showed they could not go red: two asserted before the state under test had
settled, one matched a title the Plex tile does not render.
This commit is contained in:
2026-09-08 03:03:45 +02:00
parent 84f4b20e5a
commit fdc1eabbe0
7 changed files with 282 additions and 24 deletions
@@ -188,6 +188,37 @@ describe("Home — the comfort shelf's context menu", () => {
await waitFor(() => expect(reorderComfortPins).toHaveBeenCalledWith([7, 3, 9]));
});
it("moves the tile under the pointer, not only in the request", async () => {
mount();
fireEvent.contextMenu(await screen.findByText("Comfort A"));
// Hold the refetch that follows the write: it would answer with this mock's ORIGINAL order,
// which is not what is under test. The claim here is that the tile moves under the pointer,
// before any round trip — `keepPreviousData` keeps the patched cache on screen meanwhile.
discover.mockReturnValue(new Promise(() => {}));
fireEvent.click(item("home.shelf.moveRight"));
await waitFor(() => expect(reorderComfortPins).toHaveBeenCalled());
// The RENDERED order, not just the payload sent: drop the optimistic cache write and the request
// is still perfectly right while the shelf visibly does not move, so the user clicks again.
expect(screen.getAllByText(/^Comfort [ABC]$/).map((e) => e.textContent)).toEqual([
"Comfort B",
"Comfort A",
"Comfort C",
]);
});
it("re-asks the server when a move fails instead of restoring a snapshot", async () => {
// Writing a captured `before` back was wrong twice: two overlapping moves each reverted to their
// own snapshot and the shelf settled on an order neither side held, and `before` is the WHOLE
// payload, so the rollback also discarded any rail refresh that landed mid-flight.
reorderComfortPins.mockRejectedValue(new Error("nope"));
mount();
await screen.findByText("Comfort A");
expect(discover).toHaveBeenCalledTimes(1);
fireEvent.contextMenu(screen.getByText("Comfort A"));
fireEvent.click(item("home.shelf.moveRight"));
await waitFor(() => expect(discover).toHaveBeenCalledTimes(2));
});
it("unpins with the pin id the shelf card carries", async () => {
mount();
fireEvent.contextMenu(await screen.findByText("Comfort C"));