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
+ });
+});