diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 53523402..4cf4e809 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; @@ -432,9 +432,14 @@ export default function Channels() { }, [filters, search, sort, resetPage]); const cardPager = ; - const viewTabs: TabDef[] = ( - ["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[] - ).map((v) => ({ id: v, label: t(`channels.tabs.${v}`) })); + const viewTabs: TabDef[] = useMemo( + () => + (["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[]).map((v) => ({ + id: v, + label: t(`channels.tabs.${v}`), + })), + [t], + ); const tabs = (
({ + useNavigation: () => ({ page: "channels" }), + useNavigationActions: () => ({ setPage: vi.fn() }), +})); + +import { ChannelsProvider, useChannels, useChannelsActions } from "./ChannelsProvider"; + +const wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); +const render = () => renderHook(() => ({ ...useChannels(), ...useChannelsActions() }), { wrapper }); +const VIEW_KEY = "siftlode.channelsView.1"; // account-scoped (accountKey suffixes the pinned id) + +beforeEach(() => { + localStorage.clear(); + sessionStorage.setItem("siftlode.activeAccount", "1"); // pin the tab's account + window.history.replaceState({ sfPage: "channels" }, ""); // setView only owns an entry on this page +}); + +describe("ChannelsProvider tab history", () => { + it("setView pushes a _chTab history entry and persists the tab", () => { + const { result } = render(); + expect(result.current.view).toBe("subscribed"); // default + + act(() => result.current.setView("hidden")); + + expect(result.current.view).toBe("hidden"); + expect(window.history.state).toMatchObject({ sfPage: "channels", _chTab: "hidden" }); + expect(localStorage.getItem(VIEW_KEY)).toBe("hidden"); + }); + + it("does NOT push a new entry when re-selecting the current tab", () => { + const { result } = render(); + const spy = vi.spyOn(window.history, "pushState"); + act(() => result.current.setView("subscribed")); // already the current tab + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + + it("a genuine Back (popstate carrying _chTab) restores and re-persists that tab", () => { + const { result } = render(); + act(() => result.current.setView("blocked")); + expect(result.current.view).toBe("blocked"); + + // Browser Back to an entry stamped with a different tab. + act(() => + window.dispatchEvent(new PopStateEvent("popstate", { state: { _chTab: "discovery" } })), + ); + + expect(result.current.view).toBe("discovery"); + expect(localStorage.getItem(VIEW_KEY)).toBe("discovery"); + }); + + it("ignores a popstate for an entry outside the manager (no _chTab)", () => { + const { result } = render(); + act(() => result.current.setView("hidden")); + act(() => window.dispatchEvent(new PopStateEvent("popstate", { state: { sfPage: "feed" } }))); + expect(result.current.view).toBe("hidden"); // unchanged + }); +});