// @vitest-environment jsdom // // Contract of the feed-card hover infobox (E8·S3): the trigger stays closed until opened; on open the // header (full title + channel) renders IMMEDIATELY from the Video we already hold, while the // description (videoDetail) and the channel's tags (channelDetail.tag_ids resolved against the tag // list) fill in from their on-demand queries; an empty description falls back to the "no description" // line. i18n is key-passthrough and the AnchoredPopover platform is floating-ui (hover mode → open via // focus), so we stub the observers autoUpdate wires and wrap in a FloatingTree + QueryClient. import { describe, it, expect, afterEach, beforeAll, vi } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { FloatingTree } from "@floating-ui/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), }; }); const videoDetail = vi.fn(); const channelDetail = vi.fn(); const tags = vi.fn(); vi.mock("../lib/api", () => ({ api: { videoDetail: (...a: unknown[]) => videoDetail(...a), channelDetail: (...a: unknown[]) => channelDetail(...a), tags: (...a: unknown[]) => tags(...a), }, })); import VideoInfoCard from "./VideoInfoCard"; import type { Video } from "../lib/api"; 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.clearAllMocks(); }); const video = (over: Partial