// @vitest-environment jsdom // // A channel filter's chips are named from the subscription list, which a reload has not fetched yet: // they used to read as raw UC… ids for a moment (the owner, 2026-10-03). The names seen last time now // name them until the list lands; a channel never named on this browser draws a placeholder instead. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, render, renderHook, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createElement, type ReactNode } from "react"; import { api, type FeedFilters, type ManagedChannel } from "../lib/api"; import { ACTIVE_ACCOUNT_KEY } from "../lib/activeAccount"; import { accountKey, LS } from "../lib/storage"; import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters"; import ActiveFilterChips from "./ActiveFilterChips"; import ChannelPicker from "./ChannelPicker"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), }; }); const KNOWN = "UCknown0000000000000000"; const NEW = "UCnever00000000000000000"; const filters: FeedFilters = { ...clearedFilters({ q: "" } as FeedFilters), channelIds: [KNOWN, NEW], }; const namesKey = () => accountKey(LS.channelNames, 5)!; let resolveChannels: (c: ManagedChannel[]) => void; let qc: QueryClient; const wrapper = ({ children }: { children: ReactNode }) => createElement(QueryClientProvider, { client: qc }, children); beforeEach(() => { localStorage.clear(); sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, "5"); localStorage.setItem(namesKey(), JSON.stringify({ [KNOWN]: "Remembered name" })); qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); vi.spyOn(api, "tags").mockResolvedValue([] as never); vi.spyOn(api, "channels").mockReturnValue( new Promise((r) => { resolveChannels = r; }), ); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const land = async () => { await act(async () => { resolveChannels([ { id: KNOWN, title: "Renamed since" }, { id: NEW, title: "Brand new" }, ] as ManagedChannel[]); }); }; describe("a channel filter's chip names", () => { it("name a remembered channel before the list lands, and hold the rest as pending", async () => { const { result } = renderHook(() => useActiveFilters(filters, () => {}), { wrapper }); const chip = (id: string) => result.current.find((f) => f.key === `channel:${id}`)!; expect(chip(KNOWN)).toMatchObject({ label: "Remembered name", pending: false }); expect(chip(NEW)).toMatchObject({ label: NEW, pending: true }); await land(); await waitFor(() => expect(chip(NEW)).toMatchObject({ label: "Brand new", pending: false })); // The list wins over the remembered copy, and replaces it for the next reload. expect(chip(KNOWN).label).toBe("Renamed since"); expect(JSON.parse(localStorage.getItem(namesKey())!)).toEqual({ [KNOWN]: "Renamed since", [NEW]: "Brand new", }); }); it("draw a placeholder above the feed, not the raw id", () => { const { result } = renderHook(() => useActiveFilters(filters, () => {}), { wrapper }); const { container } = render( {}} />, ); expect(screen.getAllByText("Remembered name").length).toBeGreaterThan(0); // The id is only a screen reader's (sr-only), beside a bone; nothing paints it. for (const el of screen.getAllByText(NEW)) expect(el.className).toBe("sr-only"); expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0); }); it("draw a placeholder in the filter panel's picker, then the name", async () => { const { container } = render( {}} />, { wrapper, }); screen.getByText("Remembered name"); expect(screen.getByText(NEW).className).toBe("sr-only"); expect(container.querySelectorAll(".animate-pulse")).toHaveLength(1); await land(); await waitFor(() => screen.getByText("Brand new")); expect(screen.queryByText(NEW)).toBeNull(); expect(container.querySelectorAll(".animate-pulse")).toHaveLength(0); }); it("fall back to the id when the list fails, so each chip stays told apart", async () => { localStorage.removeItem(namesKey()); vi.spyOn(api, "channels").mockRejectedValue(new Error("down")); const { result } = renderHook(() => useActiveFilters(filters, () => {}), { wrapper }); await waitFor(() => expect(result.current.find((f) => f.key === `channel:${NEW}`)).toMatchObject({ label: NEW, pending: false, }), ); }); });