// @vitest-environment jsdom // // The channel manager's card layout as a keyboard list: each card is an item, and Enter opens // the channel through the name ChannelLink renders. VirtualGrid is replaced by a stand-in that mounts // every item — jsdom has no layout for the virtualizer to measure — and registers with listNav exactly // when the real one would: when it is asked to (`keyboardNav`). import { Fragment, useRef } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, render } from "@testing-library/react"; import type { VirtualGridProps } from "./ui/VirtualGrid"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); vi.mock("./ui/VirtualGrid", async () => { const { useListNav } = await import("../lib/listNav"); return { default: function GridStandIn({ items, getKey, renderItem, keyboardNav, }: VirtualGridProps) { const ref = useRef(null); useListNav(ref, { enabled: !!keyboardNav }); return (
{items.map((item, i) => ( {renderItem(item, 0, i)} ))}
); }, }; }); import ChannelLayoutGrid from "./ChannelLayoutGrid"; import ChannelLink from "./ChannelLink"; import { type Column } from "./ui/DataTable"; afterEach(cleanup); type Ch = { id: string; title: string }; const rows: Ch[] = [ { id: "UCa", title: "Alpha" }, { id: "UCb", title: "Beta" }, ]; function press(key: string) { act(() => { (document.activeElement ?? document.body).dispatchEvent( new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), ); }); } const focusedTitle = () => (document.activeElement as HTMLElement).querySelector("[data-nav-open]")?.textContent; describe("ChannelLayoutGrid · keyboard", () => { it("walks the cards with j/k and opens the channel on Enter", () => { const opened: string[] = []; const columns: Column[] = [ { key: "channel", header: "Channel", cardPrimary: true, render: (c) => ( opened.push(c.id)} /> ), }, ]; render( c.id} />); press("j"); expect(focusedTitle()).toBe("Alpha"); press("j"); expect(focusedTitle()).toBe("Beta"); press("Enter"); expect(opened).toEqual(["UCb"]); }); }); describe('ChannelLayoutGrid · the card\'s "…"', () => { it("docks it in the heading row beside the name, not in the meta line", () => { const columns: Column[] = [ { key: "channel", header: "Channel", cardPrimary: true, render: (c) => {c.title} }, { key: "stored", header: "Stored", render: () => 12 }, { key: "actions", header: "", render: (c) => … }, ]; const { getByTestId, getByText } = render( c.id} moreKey="actions" />, ); const heading = getByText("Alpha").closest("div")!.parentElement!; expect(heading.contains(getByTestId("more-UCa"))).toBe(true); // Exactly once: a slotted column is not also painted in the meta line. expect(document.querySelectorAll('[data-testid="more-UCa"]')).toHaveLength(1); }); });