From 17b909c97f18cfd9c93811c3b691b35bd0f6edd3 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 22:32:39 +0000 Subject: [PATCH] story-kit: merge 113_story_add_test_coverage_for_usepathcompletion_hook --- .../selection/usePathCompletion.test.ts | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/frontend/src/components/selection/usePathCompletion.test.ts b/frontend/src/components/selection/usePathCompletion.test.ts index d33df8e..378edbe 100644 --- a/frontend/src/components/selection/usePathCompletion.test.ts +++ b/frontend/src/components/selection/usePathCompletion.test.ts @@ -157,4 +157,305 @@ describe("usePathCompletion hook", () => { expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/"); }); + + it("uses homeDir when input has no slash (bare partial)", async () => { + mockListDir.mockResolvedValue([ + { name: "Documents", kind: "dir" }, + { name: "Downloads", kind: "dir" }, + ]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "Doc", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(1); + }); + + expect(mockListDir).toHaveBeenCalledWith("/home/user"); + expect(result.current.matchList[0].name).toBe("Documents"); + expect(result.current.matchList[0].path).toBe("/home/user/Documents/"); + }); + + it("returns early when input has no slash and homeDir is null", async () => { + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "Doc", + setPathInput: vi.fn(), + homeDir: null, + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + // Wait for debounce + effect to fire + await waitFor(() => { + expect(result.current.matchList).toEqual([]); + }); + + expect(mockListDir).not.toHaveBeenCalled(); + }); + + it("returns empty matchList when no dirs match the fuzzy filter", async () => { + mockListDir.mockResolvedValue([ + { name: "Documents", kind: "dir" }, + { name: "Downloads", kind: "dir" }, + ]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/home/user/zzz", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(mockListDir).toHaveBeenCalled(); + }); + + // No dirs match "zzz" fuzzy filter, so matchList stays empty + expect(result.current.matchList).toEqual([]); + }); + + it("sets completionError when listDirectoryAbsolute throws an Error", async () => { + mockListDir.mockRejectedValue(new Error("Permission denied")); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/root/", + setPathInput: vi.fn(), + homeDir: null, + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.completionError).toBe("Permission denied"); + }); + }); + + it("sets generic completionError when listDirectoryAbsolute throws a non-Error", async () => { + mockListDir.mockRejectedValue("some string error"); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/root/", + setPathInput: vi.fn(), + homeDir: null, + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.completionError).toBe( + "Failed to compute suggestion.", + ); + }); + }); + + it("clears suggestionTail when selected match path does not start with input", async () => { + mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "Doc", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + // Wait for matches to load (path will be /home/user/Documents/) + await waitFor(() => { + expect(result.current.matchList.length).toBe(1); + }); + + // The match path is "/home/user/Documents/" which does NOT start with "Doc" + // so suggestionTail should be "" + expect(result.current.suggestionTail).toBe(""); + }); + + it("acceptSelectedMatch calls setPathInput with the selected match path", async () => { + mockListDir.mockResolvedValue([ + { name: "Documents", kind: "dir" }, + { name: "Downloads", kind: "dir" }, + ]); + const setPathInput = vi.fn(); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/home/user/", + setPathInput, + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(2); + }); + + act(() => { + result.current.acceptSelectedMatch(); + }); + + expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/"); + }); + + it("acceptSelectedMatch does nothing when matchList is empty", () => { + const setPathInput = vi.fn(); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "", + setPathInput, + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + act(() => { + result.current.acceptSelectedMatch(); + }); + + expect(setPathInput).not.toHaveBeenCalled(); + }); + + it("closeSuggestions clears matchList, selectedMatch, suggestionTail, and completionError", async () => { + mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/home/user/", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(1); + }); + + act(() => { + result.current.closeSuggestions(); + }); + + expect(result.current.matchList).toEqual([]); + expect(result.current.selectedMatch).toBe(0); + expect(result.current.suggestionTail).toBe(""); + expect(result.current.completionError).toBeNull(); + }); + + it("uses homeDir with trailing slash as-is", async () => { + mockListDir.mockResolvedValue([{ name: "Projects", kind: "dir" }]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "Pro", + setPathInput: vi.fn(), + homeDir: "/home/user/", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(1); + }); + + expect(mockListDir).toHaveBeenCalledWith("/home/user"); + expect(result.current.matchList[0].path).toBe("/home/user/Projects/"); + }); + + it("handles root directory listing (dir = '/')", async () => { + mockListDir.mockResolvedValue([ + { name: "home", kind: "dir" }, + { name: "etc", kind: "dir" }, + ]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/", + setPathInput: vi.fn(), + homeDir: null, + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(2); + }); + + expect(mockListDir).toHaveBeenCalledWith("/"); + expect(result.current.matchList[0].name).toBe("etc"); + expect(result.current.matchList[1].name).toBe("home"); + }); + + it("computes suggestionTail when match path starts with trimmed input", async () => { + mockListDir.mockResolvedValue([{ name: "Documents", kind: "dir" }]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/home/user/", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(1); + }); + + // path is "/home/user/Documents/" and input is "/home/user/" + // so tail should be "Documents/" + expect(result.current.suggestionTail).toBe("Documents/"); + }); + + it("setSelectedMatch updates the selected index", async () => { + mockListDir.mockResolvedValue([ + { name: "Documents", kind: "dir" }, + { name: "Downloads", kind: "dir" }, + ]); + + const { result } = renderHook(() => + usePathCompletion({ + pathInput: "/home/user/", + setPathInput: vi.fn(), + homeDir: "/home/user", + listDirectoryAbsolute: mockListDir, + debounceMs: 0, + }), + ); + + await waitFor(() => { + expect(result.current.matchList.length).toBe(2); + }); + + act(() => { + result.current.setSelectedMatch(1); + }); + + expect(result.current.selectedMatch).toBe(1); + // After selecting index 1, suggestionTail should reflect "Downloads/" + expect(result.current.suggestionTail).toBe("Downloads/"); + }); });