test(channels): cover the tab-history logic; memoize viewTabs (S2b review)

Addresses the 2 applied findings from /code-review high on 735d08c:
- Add ChannelsProvider.dom.test.tsx (4 tests): setView pushes a _chTab history entry +
  persists the tab, re-selecting the current tab pushes nothing, a genuine popstate carrying
  _chTab restores + re-persists it, and a popstate without _chTab is ignored. Real storage
  (jsdom) with the account pinned to 1; only NavigationProvider is mocked. vitest 120->124.
- Memoize viewTabs (keyed on the translation fn) so it isn't rebuilt on every Channels render.

The third finding (consolidate the hand-rolled single-select sync chips with <Tabs>) is
deferred by design into the S2c plan, where MatchToggle will be a shared segmented-control
primitive that can also absorb the sync chips.

gate-green: tsc + vitest 124 + eslint-0 + knip + prettier.
This commit is contained in:
2026-08-14 04:01:38 +02:00
parent 735d08c91d
commit ed1a2b7256
2 changed files with 82 additions and 4 deletions
+9 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys"; import { qk } from "../lib/queryKeys";
@@ -432,9 +432,14 @@ export default function Channels() {
}, [filters, search, sort, resetPage]); }, [filters, search, sort, resetPage]);
const cardPager = <Pager {...pagerProps} />; const cardPager = <Pager {...pagerProps} />;
const viewTabs: TabDef[] = ( const viewTabs: TabDef[] = useMemo(
["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[] () =>
).map((v) => ({ id: v, label: t(`channels.tabs.${v}`) })); (["subscribed", "discovery", "hidden", "blocked"] as ChannelsView[]).map((v) => ({
id: v,
label: t(`channels.tabs.${v}`),
})),
[t],
);
const tabs = ( const tabs = (
<div className="px-4 pt-4 max-w-384 mx-auto flex items-center justify-between gap-4 flex-wrap"> <div className="px-4 pt-4 max-w-384 mx-auto flex items-center justify-between gap-4 flex-wrap">
<Tabs <Tabs
@@ -0,0 +1,73 @@
// @vitest-environment jsdom
//
// ChannelsProvider's tab-history behaviour: setView pushes a `_chTab` history entry + persists the tab,
// and a genuine browser Back (popstate carrying `_chTab`) restores + re-persists it. This wiring was
// re-routed through usePersistedTab in R13 S2b, so it's exercised here rather than only by hand E2E.
//
// Uses REAL storage (jsdom localStorage/sessionStorage) with the tab's account pinned to 1, so
// usePersistedTab / useSortState / useFilterState all resolve their account-scoped keys; only
// NavigationProvider (the sole runtime dependency) is mocked, pinned to the channels page.
import { describe, it, expect, beforeEach, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import type { ReactNode } from "react";
vi.mock("./NavigationProvider", () => ({
useNavigation: () => ({ page: "channels" }),
useNavigationActions: () => ({ setPage: vi.fn() }),
}));
import { ChannelsProvider, useChannels, useChannelsActions } from "./ChannelsProvider";
const wrapper = ({ children }: { children: ReactNode }) => (
<ChannelsProvider>{children}</ChannelsProvider>
);
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
});
});