// @vitest-environment jsdom // // The video menu's download row was labelled with the download's STATE ("Downloaded", // "In queue") — a row that reads as a status yet opens the download dialog. It is "Download…" now, // the state riding along as the row's hint. import type { ReactNode } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (k: string) => k }), })); vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) })); vi.mock("./comfortPin", () => ({ pinVideo: vi.fn(), useComfortPin: () => ({ item: () => [] }), })); import { api, type Video } from "../lib/api"; import { useVideoActions } from "./useVideoActions"; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const video = { id: "v1", title: "A video", status: "new", position_seconds: 0 } as Video; function downloadRow(state: string | undefined) { vi.spyOn(api, "downloadIndex").mockResolvedValue( (state ? { v1: state } : {}) as Awaited>, ); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); const { result } = renderHook( () => useVideoActions({ onState: vi.fn(), onToggleSave: vi.fn() }, true), { wrapper }, ); return () => result.current.items(video, null).find((i) => i.id === "download")!; } describe("the video menu's download row", () => { // The state rode in a `title`, which a key or a finger never shows. it("says what it does, whatever the state, and carries the state as its note", async () => { const done = downloadRow("done"); await waitFor(() => expect(done().note).toBe("downloads.button.downloaded")); expect(done().label).toBe("downloads.menu.download"); cleanup(); const none = downloadRow(undefined); await waitFor(() => expect(none().label).toBe("downloads.menu.download")); expect(none().note).toBeUndefined(); }); }); // The feed's channel filter from a card (owner): under "Open the channel", a row that adds the card's // channel to the filter, or takes it out again when it is already in. describe("the video menu's channel-filter row", () => { const menu = (ids: string[] | null) => { vi.spyOn(api, "downloadIndex").mockResolvedValue({}); const set = vi.fn(); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); const { result } = renderHook( () => useVideoActions( { onState: vi.fn(), onToggleSave: vi.fn(), channelFilter: ids ? { ids, set } : undefined, }, true, ), { wrapper }, ); const items = result.current.items({ ...video, channel_id: "UCa" }, null); return { items, set }; }; it("adds the channel to a filter that names others, right under Open the channel", () => { const { items, set } = menu(["UCz"]); const at = items.findIndex((i) => i.id === "channel"); const row = items[at + 1]!; expect(row.id).toBe("channel-filter"); expect(row.label).toBe("card.menu.channelFilterAdd"); row.onSelect?.(); expect(set).toHaveBeenCalledWith(["UCz", "UCa"]); }); it("takes it out again when it is in", () => { const { items, set } = menu(["UCz", "UCa"]); const row = items.find((i) => i.id === "channel-filter")!; expect(row.label).toBe("card.menu.channelFilterRemove"); row.onSelect?.(); expect(set).toHaveBeenCalledWith(["UCz"]); }); it("is absent where the host gives no filter (a channel page, Home, a live search)", () => { expect(menu(null).items.some((i) => i.id === "channel-filter")).toBe(false); }); });