// @vitest-environment jsdom // // DataTable's `keyboardNav` (E11 S4c): the rows are a keyboard list — `j`/`k` walk them and Enter // clicks the element a column marks `data-nav-open`. The table renders its rows twice (a table and, // below `md`, a card list); only the copy on screen may be walked, or `j` past the last row would carry // on into the hidden one. import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, render } from "@testing-library/react"; import DataTable, { type Column } from "./DataTable"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); afterEach(cleanup); type Row = { id: string }; const rows: Row[] = [{ id: "a" }, { id: "b" }]; function table(onOpen: (id: string) => void, keyboardNav = true) { const columns: Column[] = [ { key: "name", header: "Name", cardPrimary: true, render: (r) => ( ), }, { key: "act", header: "", render: (r) => }, ]; return render( r.id} keyboardNav={keyboardNav} />, ); } function press(key: string) { act(() => { (document.activeElement ?? document.body).dispatchEvent( new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), ); }); } /** What a display:none copy answers in a browser; jsdom lays nothing out, so every copy is "visible". */ const offScreen = (el: Element | null) => Object.defineProperty(el!, "checkVisibility", { value: () => false, configurable: true }); /** The focused row: which copy (TR / DIV) and which row. */ const focused = () => { const el = document.activeElement as HTMLElement; return `${el.tagName} ${el.querySelector("[data-nav-open]")?.textContent ?? ""}`; }; describe("DataTable · keyboardNav", () => { it("walks the table's rows, stops at the last one, and opens a row on Enter", () => { const opened: string[] = []; const { container } = table((id) => opened.push(id)); offScreen(container.querySelector(".md\\:hidden")); press("j"); expect(focused()).toBe("TR a"); press("j"); expect(focused()).toBe("TR b"); // The hidden card copy follows the table in the DOM — it must not be the next stop. press("j"); expect(focused()).toBe("TR b"); press("Enter"); expect(opened).toEqual(["b"]); }); it("walks the card copy when that is the one on screen", () => { const { container } = table(() => {}); offScreen(container.querySelector("tbody")); press("j"); expect(focused()).toBe("DIV a"); }); it("leaves the rows out of the keyboard without keyboardNav", () => { const { container } = table(() => {}, false); expect(container.querySelector("[data-nav-index]")).toBeNull(); press("j"); expect(document.activeElement).toBe(document.body); }); });