fix(home): shelf honours the Plex gate; hero unpins its origin; palette asks for a surprise

- A13: with Plex off, the Home shelf and the hero's comfort tier go YouTube-only like the rails
  beside them (owner's call: hide the tiles). The pins are kept; GET /pins still lists them.
- Reorder deals the listed pins into the slots they already hold, so a partial list (hidden Plex
  pins, a stale tab) no longer puts two pins on one position; repeats are ignored.
- A21: a comfort hero carries `pinned_from`, the shelf entry it was resolved from; its menu acts on
  that pin, so a pinned show's next episode offers "Unpin" instead of creating a second pin.
- B12: the palette's "Meglepetés" asks for the surprise tier (the next press leading with it); the
  rotation carries on from there.
- B13: the surprise token names the history entry it was requested for (minted by setPage's new
  `then`, after the push) and is dropped on any other entry, so a navigation that never mounted
  Home cannot replay it later.
This commit is contained in:
2026-09-19 17:07:11 +02:00
parent c6bf84a578
commit 7b94e9a651
15 changed files with 291 additions and 60 deletions
+33 -2
View File
@@ -52,6 +52,7 @@ vi.mock("../lib/api", () => ({
downloadIndex: () => Promise.resolve({}), // the cards' download buttons read it
},
DISCOVER_BUDGETS: ["short", "medium", "long", "any"],
HERO_TIERS: ["resume", "comfort", "surprise"],
discoverCardKey: (c: { source: string; id: string }) => `${c.source}:${c.id}`,
}));
@@ -609,8 +610,38 @@ describe("Home", () => {
);
expect(await screen.findByText("Rerolled")).toBeTruthy();
expect(discoverPick).toHaveBeenCalledTimes(1);
// …and on the press count the server reads as "which tier answers first".
expect(discoverPick).toHaveBeenCalledWith("any", expect.any(Number), [], 1);
// …and on the press count the server reads as "which tier answers first": 2 leads with the
// SURPRISE tier, which is what a command labelled "Meglepetés" promised. A plain next press (1)
// led with the shelf, so anyone with one got "A polcodról" instead (owner's call, 2026-09-19).
expect(discoverPick).toHaveBeenCalledWith("any", expect.any(Number), [], 2);
});
// The rotation carries on from the requested tier: the page's own button after a palette surprise
// is the NEXT tier (resume), not a second surprise and not a jump back to press 1.
it("lets the button's next press carry the rotation on from the requested tier", async () => {
discoverPick.mockResolvedValue({
budget: "any",
hero: { tier: "surprise", card: ytCard("v9", "Rerolled") },
});
requestSurprise();
mount();
expect(await screen.findByText("Rerolled")).toBeTruthy();
fireEvent.click(screen.getByText("home.hero.reroll"));
await waitFor(() => expect(discoverPick).toHaveBeenCalledTimes(2));
expect(discoverPick.mock.lastCall?.[3]).toBe(3);
});
// A token minted for a navigation that never mounted Home (a failed lazy chunk, a Back before
// the module arrived) must not replay on the NEXT visit: it names the history entry it was
// requested for, and Home mounting on any other entry drops it.
it("drops a surprise request that was made for another history entry", async () => {
requestSurprise();
window.history.pushState(null, "");
mount();
await screen.findByText("Hero v1");
expect(discoverPick).not.toHaveBeenCalled();
// …and it is spent, not merely skipped: the entry it named does not revive it either.
expect(takeSurprise()).toBe(false);
});
it("says the shelf is empty rather than hiding it", async () => {