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:
@@ -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 = <Pager {...pagerProps} />;
|
||||
|
||||
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 = (
|
||||
<div className="px-4 pt-4 max-w-384 mx-auto flex items-center justify-between gap-4 flex-wrap">
|
||||
<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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user