diff --git a/frontend/src/components/NavigationProvider.dom.test.tsx b/frontend/src/components/NavigationProvider.dom.test.tsx index a71d90dd..47078c5d 100644 --- a/frontend/src/components/NavigationProvider.dom.test.tsx +++ b/frontend/src/components/NavigationProvider.dom.test.tsx @@ -176,6 +176,19 @@ describe("NavigationProvider · setPage's same-page guard", () => { }); }); +describe("NavigationProvider · Back", () => { + // A tab that had a since-removed module open, reloaded onto the new build: its older history + // entries still name that page, and Back lands on one of them. + it("lands on the feed when the entry names a page this build does not have", () => { + const { result } = render(); + act(() => result.current.setPage("plex")); + act(() => { + window.dispatchEvent(new PopStateEvent("popstate", { state: { sfPage: "messages" } })); + }); + expect(result.current.page).toBe("feed"); + }); +}); + describe("NavigationProvider · never under an open overlay", () => { it("setPage closes the overlay first and pushes only once its entry is reclaimed", async () => { const { result } = render(); diff --git a/frontend/src/components/NavigationProvider.tsx b/frontend/src/components/NavigationProvider.tsx index 158de493..170f7cdd 100644 --- a/frontend/src/components/NavigationProvider.tsx +++ b/frontend/src/components/NavigationProvider.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react import { useLatest } from "../lib/useLatest"; import { useTranslation } from "react-i18next"; import { useMe } from "../lib/useMe"; -import { urlWithParams, type Page } from "../lib/urlState"; +import { isPage, urlWithParams, type Page } from "../lib/urlState"; import { LOGS_KEYS } from "../lib/logsUrlState"; import { pageTitleKey } from "../lib/pageMeta"; import { useDocumentObjectTitle } from "../lib/documentTitle"; @@ -197,7 +197,10 @@ export function NavigationProvider({ children }: { children: ReactNode }) { window.history.replaceState({ ...rest, sfPage: mountPage }, "", url); } function onPop(e: PopStateEvent) { - const p = (e.state?.sfPage as Page) ?? "feed"; + // An entry can name a page this build no longer has (one pushed before a module was removed, + // reached by Back after a reload onto the new build); that lands on the feed, not on a crash. + const sf: unknown = e.state?.sfPage; + const p: Page = typeof sf === "string" && isPage(sf) ? sf : "feed"; setPageState(p); setAccountRaw(PAGE_KEY, p); // The YouTube-search sub-view and the channel page ride in history.state, so Back/Forward