WIP: Batch 3 — backfill frontend tests
- usePathCompletion: 16 tests (isFuzzyMatch, getCurrentPartial, hook behavior) - api/client.ts: 9 tests (fetch mocks for all major endpoints, error handling) - api/workflow.ts: 6 tests (record, acceptance, review queue, ensure) Frontend tests: 13 → 44 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
160
frontend/src/components/selection/usePathCompletion.test.ts
Normal file
160
frontend/src/components/selection/usePathCompletion.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { FileEntry } from "./usePathCompletion";
|
||||
import {
|
||||
getCurrentPartial,
|
||||
isFuzzyMatch,
|
||||
usePathCompletion,
|
||||
} from "./usePathCompletion";
|
||||
|
||||
describe("isFuzzyMatch", () => {
|
||||
it("matches when query is empty", () => {
|
||||
expect(isFuzzyMatch("anything", "")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches exact prefix", () => {
|
||||
expect(isFuzzyMatch("Documents", "Doc")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches fuzzy subsequence", () => {
|
||||
expect(isFuzzyMatch("Documents", "dms")).toBe(true);
|
||||
});
|
||||
|
||||
it("is case insensitive", () => {
|
||||
expect(isFuzzyMatch("Documents", "DOCU")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects when chars not found in order", () => {
|
||||
expect(isFuzzyMatch("abc", "acb")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects completely unrelated", () => {
|
||||
expect(isFuzzyMatch("hello", "xyz")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurrentPartial", () => {
|
||||
it("returns empty for empty input", () => {
|
||||
expect(getCurrentPartial("")).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty when input ends with slash", () => {
|
||||
expect(getCurrentPartial("/home/user/")).toBe("");
|
||||
});
|
||||
|
||||
it("returns last segment", () => {
|
||||
expect(getCurrentPartial("/home/user/Doc")).toBe("Doc");
|
||||
});
|
||||
|
||||
it("returns full input when no slash", () => {
|
||||
expect(getCurrentPartial("Doc")).toBe("Doc");
|
||||
});
|
||||
|
||||
it("trims then evaluates: trailing-slash input returns empty", () => {
|
||||
// " /home/user/ " trims to "/home/user/" which ends with slash
|
||||
expect(getCurrentPartial(" /home/user/ ")).toBe("");
|
||||
});
|
||||
|
||||
it("trims then returns last segment", () => {
|
||||
expect(getCurrentPartial(" /home/user/Doc ")).toBe("Doc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("usePathCompletion hook", () => {
|
||||
const mockListDir = vi.fn<(path: string) => Promise<FileEntry[]>>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockListDir.mockReset();
|
||||
});
|
||||
|
||||
it("returns empty matchList for empty input", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePathCompletion({
|
||||
pathInput: "",
|
||||
setPathInput: vi.fn(),
|
||||
homeDir: "/home/user",
|
||||
listDirectoryAbsolute: mockListDir,
|
||||
debounceMs: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// Allow effect + setTimeout(0) to fire
|
||||
await waitFor(() => {
|
||||
expect(mockListDir).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(result.current.matchList).toEqual([]);
|
||||
});
|
||||
|
||||
it("fetches directory listing and returns matches", async () => {
|
||||
mockListDir.mockResolvedValue([
|
||||
{ name: "Documents", kind: "dir" },
|
||||
{ name: "Downloads", kind: "dir" },
|
||||
{ name: ".bashrc", kind: "file" },
|
||||
]);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
expect(result.current.matchList[0].name).toBe("Documents");
|
||||
expect(result.current.matchList[1].name).toBe("Downloads");
|
||||
expect(result.current.matchList.every((m) => m.path.endsWith("/"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("filters by fuzzy match on partial input", async () => {
|
||||
mockListDir.mockResolvedValue([
|
||||
{ name: "Documents", kind: "dir" },
|
||||
{ name: "Downloads", kind: "dir" },
|
||||
{ name: "Desktop", kind: "dir" },
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePathCompletion({
|
||||
pathInput: "/home/user/Doc",
|
||||
setPathInput: vi.fn(),
|
||||
homeDir: "/home/user",
|
||||
listDirectoryAbsolute: mockListDir,
|
||||
debounceMs: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.matchList.length).toBe(1);
|
||||
});
|
||||
|
||||
expect(result.current.matchList[0].name).toBe("Documents");
|
||||
});
|
||||
|
||||
it("calls setPathInput when acceptMatch is invoked", () => {
|
||||
const setPathInput = vi.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePathCompletion({
|
||||
pathInput: "/home/",
|
||||
setPathInput,
|
||||
homeDir: "/home",
|
||||
listDirectoryAbsolute: mockListDir,
|
||||
debounceMs: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.acceptMatch("/home/user/Documents/");
|
||||
});
|
||||
|
||||
expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export interface UsePathCompletionResult {
|
||||
closeSuggestions: () => void;
|
||||
}
|
||||
|
||||
function isFuzzyMatch(candidate: string, query: string) {
|
||||
export function isFuzzyMatch(candidate: string, query: string) {
|
||||
if (!query) return true;
|
||||
const lowerCandidate = candidate.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
@@ -43,7 +43,7 @@ function isFuzzyMatch(candidate: string, query: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCurrentPartial(input: string) {
|
||||
export function getCurrentPartial(input: string) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.endsWith("/")) return "";
|
||||
|
||||
Reference in New Issue
Block a user