// @vitest-environment jsdom // // The feed's toolbar, on the feed and on a channel page. The filter pills name their group by a glyph // (the word moved to the tooltip and the accessible name), Sort + View end the first row instead of // taking a row of their own, and a channel page drops the count pill: its header already says how // many videos the channel has. import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), })); vi.mock("../../features/wizard/wizardContext", () => ({ useWizard: () => ({ openWizard: vi.fn() }), })); vi.mock("./VirtualFeed", () => ({ default: () => null })); vi.mock("../../features/filters/SelectedViewChip", () => ({ default: () => null })); vi.mock("../../features/filters/ActiveFilterChips", () => ({ default: () => null })); import Feed from "./Feed"; import { api, DEFAULT_TAG_MODES, type FeedFilters } from "../../lib/api"; import { qk } from "../../lib/queryKeys"; import { PageToolbarSlot } from "../../components/ui/pageToolbarSlot"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const FILTERS: FeedFilters = { tags: [], tagModes: DEFAULT_TAG_MODES, channelIds: [], q: "", sort: "newest", scope: "my", includeNormal: true, includeShorts: false, includeLive: false, show: "unwatched", }; const slot = document.body.appendChild(document.createElement("div")); async function renderFeed(channelScoped = false) { vi.spyOn(api, "feed").mockResolvedValue({ items: [], next_cursor: null } as never); vi.spyOn(api, "feedSummary").mockResolvedValue({ count: 12, facets: {} } as never); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); qc.setQueryData(qk.me(), { id: 1, can_write: true, prefs: {} }); render( {}} view="cards" setView={() => {}} canRead ytSearch="" onYtSearch={() => {}} onExitYtSearch={() => {}} channelScoped={channelScoped} /> , ); // The toolbar renders once the first page has answered. await screen.findByTestId("feed-show"); } const trigger = (name: string) => screen.getAllByRole("button").find((b) => b.getAttribute("aria-label")?.startsWith(`${name}:`))!; const sortTrigger = () => trigger("feed.sortLabel"); // The group's glyph, not the trigger's own chevron: the glyph sits in its aria-hidden wrapper. const glyph = (el: HTMLElement) => el.querySelector('[aria-hidden="true"] > svg'); describe("Feed toolbar", () => { it.each([ ["feed-show", "feed.showLabel"], ["feed-content", "feed.contentLabel"], ])("the %s pill shows a glyph, not its group's word, and names itself", async (id, word) => { await renderFeed(); const pill = screen.getByTestId(id); expect(pill.textContent).not.toContain(word); expect(glyph(pill)).not.toBeNull(); expect(pill.getAttribute("title")).toBe(word); expect(pill.getAttribute("aria-label")).toMatch(new RegExp(`^${word}: `)); }); it("the sort pill shows a glyph, not the word Sort, and names itself", async () => { await renderFeed(); const sort = sortTrigger(); expect(sort.textContent).not.toContain("feed.sortLabel"); expect(glyph(sort)).not.toBeNull(); expect(sort.getAttribute("title")).toBe("feed.sortLabel"); }); it.each([false, true])( "Sort and View end the filters' row instead of a row of their own (channel page: %s)", async (channelScoped) => { await renderFeed(channelScoped); const row = screen.getByTestId("feed-show").parentElement!; expect(row.contains(sortTrigger())).toBe(true); expect(row.contains(trigger("feed.viewLabel"))).toBe(true); }, ); it("a channel page shows no count pill; the feed keeps it", async () => { await renderFeed(true); expect(screen.queryByTestId("item-count")).toBeNull(); cleanup(); await renderFeed(false); expect(screen.getByTestId("item-count")).toBeTruthy(); }); });