// @vitest-environment happy-dom import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { useState } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DirAutocomplete } from "./DirAutocomplete.js"; vi.mock("../../api.js", () => ({ listDir: vi.fn(), })); import { listDir } from "../../api.js"; const mockListDir = vi.mocked(listDir); afterEach(() => { cleanup(); vi.clearAllMocks(); }); function Harness({ initial = "" }: { initial?: string }) { const [value, setValue] = useState(initial); return ; } // `fireEvent.focus`/`.blur` dispatch the event without moving the browser's // real focus, so `document.activeElement` (which the dropdown's fetch guards // against) never changes — calling the DOM method directly does both. function focus(el: HTMLElement): void { act(() => el.focus()); } function blur(el: HTMLElement): void { act(() => el.blur()); } describe("DirAutocomplete", () => { it("lists entries matching the trailing segment on focus", async () => { mockListDir.mockResolvedValue({ path: "/home/n", entries: ["alpha", "beta"] }); render(); focus(screen.getByPlaceholderText("cwd")); expect(await screen.findByRole("option", { name: "alpha/" })).not.toBeNull(); expect(screen.queryByRole("option", { name: "beta/" })).toBeNull(); }); it("Tab commits the highlighted entry and does not close the field", async () => { mockListDir.mockResolvedValue({ path: "/home/n", entries: ["alpha"] }); render(); const input = screen.getByPlaceholderText("cwd") as HTMLInputElement; focus(input); await screen.findByRole("option", { name: "alpha/" }); fireEvent.keyDown(input, { key: "Tab" }); expect(input.value).toBe("/home/n/alpha/"); }); it("arrows move the highlight and Enter accepts it without submitting", async () => { mockListDir.mockResolvedValue({ path: "/home/n", entries: ["alpha", "beta"] }); render(); const input = screen.getByPlaceholderText("cwd") as HTMLInputElement; focus(input); await screen.findByRole("option", { name: "alpha/" }); fireEvent.keyDown(input, { key: "ArrowDown" }); const notPrevented = fireEvent.keyDown(input, { key: "Enter" }); expect(input.value).toBe("/home/n/beta/"); expect(notPrevented).toBe(false); // preventDefault called -> event "processed" }); it("Escape closes the dropdown and stops the key from bubbling", async () => { mockListDir.mockResolvedValue({ path: "/home/n", entries: ["alpha"] }); const onKeyDown = vi.fn(); render(
, ); const input = screen.getByPlaceholderText("cwd"); focus(input); await screen.findByRole("option", { name: "alpha/" }); fireEvent.keyDown(input, { key: "Escape" }); await waitFor(() => expect(screen.queryByRole("option")).toBeNull()); expect(onKeyDown).not.toHaveBeenCalled(); }); it("does not reopen on refocus until the operator types again", async () => { mockListDir.mockResolvedValue({ path: "/home/n", entries: ["alpha"] }); render(); const input = screen.getByPlaceholderText("cwd"); focus(input); await screen.findByRole("option", { name: "alpha/" }); fireEvent.keyDown(input, { key: "Escape" }); await waitFor(() => expect(screen.queryByRole("option")).toBeNull()); blur(input); focus(input); await new Promise((r) => setTimeout(r, 10)); expect(screen.queryByRole("option")).toBeNull(); fireEvent.change(input, { target: { value: "/home/n/al" } }); expect(await screen.findByRole("option", { name: "alpha/" })).not.toBeNull(); }); });