import { render } from "@solidjs/testing-library"; import { createSignal, flush } from "solid-js"; import { describe, expect, test, vi } from "vitest"; import History from "../../src/components/History"; import type { GraphRow } from "../../src/lib/bindings"; import type { GraphStatus, GraphView } from "../../src/lib/graph"; import { branchedHistory, linearHistory } from "../fixtures/rows"; /** A stand-in for the real view, so these tests never touch IPC. */ function view( rows: GraphRow[], status: GraphStatus = "streaming", ): GraphView & { loadMore: ReturnType } { const [current] = createSignal(rows); const [currentStatus] = createSignal(status); return { rows: current, status: currentStatus, message: () => undefined, show: vi.fn(), loadMore: vi.fn(), } as unknown as GraphView & { loadMore: ReturnType }; } /** Which row currently has focus. */ function focusedRow(): string | undefined { return (document.activeElement as HTMLElement | null)?.dataset.row; } function press(key: string) { document.activeElement?.dispatchEvent( new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), ); } describe("History keyboard navigation", () => { test("is a single tab stop, not one per commit", async () => { // A history is thousands of rows long; tabbing through it would be a trap. const { container } = render(() => ); await flush(); const stops = [...container.querySelectorAll("button")].filter( (button) => button.tabIndex === 0, ); expect(stops).toHaveLength(1); expect(stops[0]?.dataset.row).toBe("0"); }); test("moves one row per arrow press", async () => { // Regression: signal writes are batched, so computing the next row from a // plain read of the current one went stale and several presses in quick // succession only moved one row. const { container } = render(() => ); await flush(); container.querySelector('[data-row="0"]')?.focus(); for (let index = 0; index < 5; index++) press("ArrowDown"); expect(focusedRow()).toBe("5"); }); test("moves back up one row at a time", async () => { const { container } = render(() => ); await flush(); container.querySelector('[data-row="10"]')?.focus(); press("ArrowUp"); press("ArrowUp"); expect(focusedRow()).toBe("8"); }); test("jumps a page at a time", async () => { const { container } = render(() => ); await flush(); container.querySelector('[data-row="0"]')?.focus(); press("PageDown"); expect(focusedRow()).toBe("20"); press("PageUp"); expect(focusedRow()).toBe("0"); }); test("Home and End reach both ends", async () => { const { container } = render(() => ); await flush(); container.querySelector('[data-row="10"]')?.focus(); press("End"); expect(focusedRow()).toBe("49"); press("Home"); expect(focusedRow()).toBe("0"); }); test("stops at the ends rather than wrapping or going out of range", async () => { const { container } = render(() => ); await flush(); container.querySelector('[data-row="0"]')?.focus(); press("ArrowUp"); expect(focusedRow()).toBe("0"); press("End"); press("ArrowDown"); expect(focusedRow()).toBe("2"); }); test("leaves keys it does not handle to the browser", async () => { const { container } = render(() => ); await flush(); const first = container.querySelector('[data-row="0"]'); first?.focus(); const event = new KeyboardEvent("keydown", { key: "a", bubbles: true, cancelable: true }); first?.dispatchEvent(event); expect(event.defaultPrevented).toBe(false); expect(focusedRow()).toBe("0"); }); test("marks exactly one row as current", async () => { const { container } = render(() => ); await flush(); container.querySelector('[data-row="0"]')?.focus(); press("ArrowDown"); await flush(); const current = container.querySelectorAll("[aria-current]"); expect(current).toHaveLength(1); expect((current[0] as HTMLElement).dataset.row).toBe("1"); }); }); describe("History rendering", () => { test("is a list, so assistive tech announces how long it is", async () => { const { container } = render(() => ); await flush(); const list = container.querySelector("ol"); expect(list).not.toBeNull(); expect(list?.children).toHaveLength(5); }); test("shows each commit's summary, author and short id", async () => { const { container, getByText } = render(() => ); await flush(); expect(getByText("Commit number 0")).toBeTruthy(); expect(container.textContent).toContain("Fixture"); // Seven characters, the way git abbreviates. expect(container.textContent).toContain("0000000"); }); test("gives every commit a machine readable date", async () => { const { container } = render(() => ); await flush(); const times = container.querySelectorAll("time"); expect(times).toHaveLength(2); expect(times[0]?.getAttribute("datetime")).toBe("2023-11-14T22:13:20.000Z"); }); test("names what each ref badge is, since colour alone does not say", async () => { const { container } = render(() => ); await flush(); expect(container.textContent).toContain("current branch"); expect(container.textContent).toContain("tag"); }); test("says so when there is nothing to show", async () => { const { getByText } = render(() => ); await flush(); expect(getByText("No commits to show.")).toBeTruthy(); }); test("says it is working while the first rows are on their way", async () => { const { getByText } = render(() => ); await flush(); expect(getByText("Reading history…")).toBeTruthy(); }); });