Files
AgentCompany/tests/unit/keyboard.test.ts
T
ZacharyZhang-NY d77486504d feat(01-04): add TanStack Virtual list virtualization and keyboard navigation hook
- Install @tanstack/react-virtual for virtualized list rendering
- Create useRovingTabIndex hook in src/renderer/lib/keyboard.ts for arrow-key navigation
- Add virtualization to 6 lists: TaskQueue, ApprovalList, ActivityList, CostList, AgentRoster, MessageList
- MessageList uses measureElement for variable-height message items with auto-scroll
- All 6 lists have role="listbox" with role="option" on rows for accessibility
- All 6 lists support ArrowDown/ArrowUp/j/k/Home/End keyboard navigation
- Add 8 unit tests for useRovingTabIndex covering all key bindings and edge cases
2026-03-22 02:08:43 -04:00

153 lines
5.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { useRovingTabIndex } from "@renderer/lib/keyboard";
// Since vitest runs in Node (no DOM), we test the hook logic
// by extracting and testing the pure callback functions directly.
// We create a minimal mock of React hooks to isolate the logic.
// -- Helper to create a mock KeyboardEvent-like object --
function mockKeyEvent(key: string): {
key: string;
preventDefault: () => void;
defaultPrevented: boolean;
} {
let prevented = false;
return {
key,
preventDefault() {
prevented = true;
},
get defaultPrevented() {
return prevented;
},
};
}
describe("useRovingTabIndex", () => {
it("returns containerRef, focusedIndex (default 0), handleKeyDown, getTabIndex", () => {
const result = useRovingTabIndex(5);
expect(result).toHaveProperty("containerRef");
expect(result).toHaveProperty("focusedIndex");
expect(result).toHaveProperty("handleKeyDown");
expect(result).toHaveProperty("getTabIndex");
expect(result.focusedIndex).toBe(0);
expect(typeof result.handleKeyDown).toBe("function");
expect(typeof result.getTabIndex).toBe("function");
});
it("ArrowDown increments focusedIndex, wraps from last to 0", () => {
const result = useRovingTabIndex(3);
expect(result.focusedIndex).toBe(0);
// Press ArrowDown once -> index 1
const event1 = mockKeyEvent("ArrowDown");
result.handleKeyDown(event1 as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(1);
expect(event1.defaultPrevented).toBe(true);
// Press ArrowDown again -> index 2
const event2 = mockKeyEvent("ArrowDown");
result.handleKeyDown(event2 as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(2);
// Press ArrowDown again -> wraps to 0
const event3 = mockKeyEvent("ArrowDown");
result.handleKeyDown(event3 as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(0);
});
it("ArrowUp decrements focusedIndex, wraps from 0 to last", () => {
const result = useRovingTabIndex(3);
expect(result.focusedIndex).toBe(0);
// Press ArrowUp at 0 -> wraps to 2
const event1 = mockKeyEvent("ArrowUp");
result.handleKeyDown(event1 as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(2);
expect(event1.defaultPrevented).toBe(true);
// Press ArrowUp at 2 -> index 1
const event2 = mockKeyEvent("ArrowUp");
result.handleKeyDown(event2 as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(1);
});
it("Home sets focusedIndex to 0", () => {
const result = useRovingTabIndex(5);
// Move to index 3 first
result.handleKeyDown(mockKeyEvent("ArrowDown") as unknown as React.KeyboardEvent);
result.handleKeyDown(mockKeyEvent("ArrowDown") as unknown as React.KeyboardEvent);
result.handleKeyDown(mockKeyEvent("ArrowDown") as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(3);
// Press Home -> index 0
const event = mockKeyEvent("Home");
result.handleKeyDown(event as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(0);
expect(event.defaultPrevented).toBe(true);
});
it("End sets focusedIndex to itemCount - 1", () => {
const result = useRovingTabIndex(5);
expect(result.focusedIndex).toBe(0);
// Press End -> index 4
const event = mockKeyEvent("End");
result.handleKeyDown(event as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(4);
expect(event.defaultPrevented).toBe(true);
});
it("getTabIndex returns 0 for focusedIndex, -1 for others", () => {
const result = useRovingTabIndex(5);
// Default focusedIndex is 0
expect(result.getTabIndex(0)).toBe(0);
expect(result.getTabIndex(1)).toBe(-1);
expect(result.getTabIndex(2)).toBe(-1);
expect(result.getTabIndex(4)).toBe(-1);
// Move to index 2
result.handleKeyDown(mockKeyEvent("ArrowDown") as unknown as React.KeyboardEvent);
result.handleKeyDown(mockKeyEvent("ArrowDown") as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(2);
expect(result.getTabIndex(0)).toBe(-1);
expect(result.getTabIndex(2)).toBe(0);
});
it("j key acts same as ArrowDown, k key acts same as ArrowUp", () => {
const result = useRovingTabIndex(3);
expect(result.focusedIndex).toBe(0);
// j -> index 1
const jEvent = mockKeyEvent("j");
result.handleKeyDown(jEvent as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(1);
expect(jEvent.defaultPrevented).toBe(true);
// j -> index 2
result.handleKeyDown(mockKeyEvent("j") as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(2);
// k -> index 1
const kEvent = mockKeyEvent("k");
result.handleKeyDown(kEvent as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(1);
expect(kEvent.defaultPrevented).toBe(true);
// k -> index 0
result.handleKeyDown(mockKeyEvent("k") as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(0);
});
it("handleKeyDown is no-op when itemCount is 0", () => {
const result = useRovingTabIndex(0);
expect(result.focusedIndex).toBe(0);
const event = mockKeyEvent("ArrowDown");
result.handleKeyDown(event as unknown as React.KeyboardEvent);
expect(result.focusedIndex).toBe(0);
// preventDefault should NOT be called when itemCount is 0
expect(event.defaultPrevented).toBe(false);
});
});