feat(nav): the live YouTube search and Home's budget are named in the address
A YouTube search is `?yt=<term>` (+ `ytn` for a non-default count), so a reload, a link or Back/Forward reopens it. It runs by itself only where it costs no quota: the free page scrape, a search asked for in this document, or results still cached. An API search reopened any other way shows its term and waits for "Search YouTube again". Home's time budget is `hb`; the question's draw stays on the history entry. An e2e spec opens every page's bookmark in a fresh browser context; CLAUDE.md and docs/development.md name the module URL codec as the place for view state.
This commit is contained in:
@@ -174,8 +174,9 @@ function mountWithClient() {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Home keeps its question on the history entry (Back restores it): each test starts a fresh one.
|
||||
window.history.replaceState(null, "");
|
||||
// Home keeps its question on the history entry (Back restores it) and its budget in the address:
|
||||
// each test starts a fresh one of both.
|
||||
window.history.replaceState(null, "", "/");
|
||||
discover.mockReset();
|
||||
discoverPick.mockReset();
|
||||
setPage.mockReset();
|
||||
@@ -636,6 +637,37 @@ describe("Home", () => {
|
||||
await waitFor(() => expect(discoverPick.mock.lastCall).toEqual(asked));
|
||||
});
|
||||
|
||||
it("names the chosen budget in the address, and the default by leaving it out", async () => {
|
||||
mount();
|
||||
await screen.findByText("Hero v1");
|
||||
fireEvent.click(screen.getByText("home.budget.long"));
|
||||
await waitFor(() => expect(window.location.search).toBe("?hb=long"));
|
||||
fireEvent.click(screen.getByText("home.budget.any"));
|
||||
await waitFor(() => expect(window.location.search).toBe(""));
|
||||
});
|
||||
|
||||
it("opens on the budget a link names — the question's draw is not the link's", async () => {
|
||||
window.history.replaceState(
|
||||
{ sfHome: { budget: "short", seed: 7, press: 1, seen: ["youtube:v1"] } },
|
||||
"",
|
||||
"/?page=home&hb=long",
|
||||
);
|
||||
mount();
|
||||
await waitFor(() => expect(discover).toHaveBeenLastCalledWith("long", 0, "en"));
|
||||
expect(discoverPick).not.toHaveBeenCalled(); // a question left under another budget is not reused
|
||||
});
|
||||
|
||||
it("an address naming no budget opens on the default, over a question left under another", async () => {
|
||||
window.history.replaceState(
|
||||
{ sfHome: { budget: "short", seed: 7, press: 1, seen: [] } },
|
||||
"",
|
||||
"/?page=home",
|
||||
);
|
||||
mount();
|
||||
await waitFor(() => expect(discover).toHaveBeenLastCalledWith("any", 0, "en"));
|
||||
expect(discoverPick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Measured in the browser (2026-09-07), before the fix: the palette's "Meglepetés" arrived at Home
|
||||
// as `start=2` — two picks deep before the page had painted once — because StrictMode invokes a
|
||||
// mount effect twice with the same render snapshot, and a reroll is not idempotent. The token is
|
||||
|
||||
@@ -79,6 +79,8 @@ import {
|
||||
} from "../../components/ui/mediaCardStyles";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
import { useOpenLinkedPlayer } from "../../features/player/useOpenLinkedPlayer";
|
||||
import { budgetFrom, budgetParams, DEFAULT_BUDGET } from "../../lib/homeUrlState";
|
||||
import { useUrlMirror } from "../../lib/useUrlMirror";
|
||||
|
||||
// The Home surface — "Mit nézzek most?".
|
||||
//
|
||||
@@ -701,6 +703,15 @@ function readSavedQuestion(): SavedQuestion | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** The question the page opens with: the budget its address names (a link wins, and an address
|
||||
* naming none means the default), with the rest of what the entry was left with only when that was
|
||||
* asked under the same budget. */
|
||||
function openingQuestion(): { budget: DiscoverBudget; saved: SavedQuestion | null } {
|
||||
const budget = budgetFrom(new URLSearchParams(window.location.search)) ?? DEFAULT_BUDGET;
|
||||
const saved = readSavedQuestion();
|
||||
return { budget, saved: saved?.budget === budget ? saved : null };
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
@@ -710,8 +721,10 @@ export default function Home() {
|
||||
// The question on screen lives on this page's history ENTRY as well: opening a card leaves Home
|
||||
// (it unmounts), and Back used to come back to "Doesn't matter" and a different hero — whose card
|
||||
// then left the Continue rail, so the keyboard landed on its neighbour (prod UAT 2026-09-19).
|
||||
const [saved] = useState(readSavedQuestion);
|
||||
const [budget, setBudget] = useState<DiscoverBudget>(saved?.budget ?? "any");
|
||||
// The budget alone is also in the address (lib/homeUrlState): a setting, unlike the draw under it.
|
||||
const [{ budget: openedBudget, saved }] = useState(openingQuestion);
|
||||
const [budget, setBudget] = useState<DiscoverBudget>(openedBudget);
|
||||
useUrlMirror(budgetParams(budget));
|
||||
// 0 = "the hero the page loaded with". Any other value is a reroll, answered by its own query so
|
||||
// pressing the button does not refetch the rails under the user's cursor.
|
||||
const [seed, setSeed] = useState(saved?.seed ?? 0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Home } from "lucide-react";
|
||||
import { lazyWithPreload } from "../../lib/lazyWithPreload";
|
||||
import type { ModuleDef } from "../../lib/moduleDef";
|
||||
import { HOME_KEYS } from "../../lib/homeUrlState";
|
||||
|
||||
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
|
||||
export const homeModule: ModuleDef = {
|
||||
@@ -11,8 +12,8 @@ export const homeModule: ModuleDef = {
|
||||
icon: Home,
|
||||
Page: lazyWithPreload(() => import("./Home")),
|
||||
maxWidth: "80rem",
|
||||
// Nothing of its own: the question is an answer, not a setting — the same question draws other
|
||||
// cards once what you have watched moves on.
|
||||
urlKeys: [],
|
||||
// Its time budget, a setting. Not the question's draw: the same question draws other cards once
|
||||
// what you have watched moves on (lib/homeUrlState).
|
||||
urlKeys: HOME_KEYS,
|
||||
playsVideos: true,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user