Files
kandev/apps/web/components/folder-picker.test.tsx
T
2026-09-06 11:32:14 +01:00

187 lines
7.6 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { type ReactElement, type ReactNode, cloneElement } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FolderPicker } from "./folder-picker";
import { listDirectory, type DirectoryListing } from "@/lib/api/domains/fs-api";
let setPopoverOpen: ((open: boolean) => void) | undefined;
vi.mock("@kandev/ui/popover", () => ({
Popover: ({
children,
onOpenChange,
}: {
children: ReactNode;
onOpenChange: (open: boolean) => void;
}) => {
setPopoverOpen = onOpenChange;
return <div>{children}</div>;
},
PopoverTrigger: ({ children }: { children: ReactElement<{ onClick?: () => void }> }) =>
cloneElement(children, { onClick: () => setPopoverOpen?.(true) }),
PopoverContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("@kandev/ui/tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/lib/api/domains/fs-api", async (importOriginal) => {
const original = await importOriginal<typeof import("@/lib/api/domains/fs-api")>();
return { ...original, listDirectory: vi.fn() };
});
const mockedListDirectory = vi.mocked(listDirectory);
const BUTTON_ROLE = "button";
const TRIGGER_TEST_ID = "folder-picker-trigger";
const VIRTUAL_ROOT = "/";
const DRIVE_E_ROOT = "E:\\";
const SUCCESS_PATH = "E:\\Success";
const SUCCESS_NAME = "Success";
afterEach(() => {
cleanup();
mockedListDirectory.mockReset();
setPopoverOpen = undefined;
delete (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
delete (window as Window & { __KANDEV_BOOT_PAYLOAD__?: unknown }).__KANDEV_BOOT_PAYLOAD__;
});
describe("FolderPicker", () => {
it("preserves the separator when displaying the POSIX root", () => {
render(<FolderPicker value="/" onChange={vi.fn()} placeholder="Pick a folder" />);
expect(screen.getByTestId(TRIGGER_TEST_ID).textContent).toBe("/");
});
it("preserves the separator when displaying a Windows drive root", () => {
render(<FolderPicker value="C:\\" onChange={vi.fn()} />);
expect(screen.getByTestId(TRIGGER_TEST_ID).textContent).toBe("C:\\");
});
it("uses the native picker in a Tauri WebView without listing directories over HTTP", async () => {
const invoke = vi.fn(async () => ({ status: "selected", path: "/Users/example/Code" }));
Object.defineProperty(window, "__TAURI_INTERNALS__", {
configurable: true,
value: { invoke },
});
Object.defineProperty(window, "__KANDEV_BOOT_PAYLOAD__", {
configurable: true,
value: { runtime: { nativeFolderPickerAvailable: true } },
});
const onChange = vi.fn();
render(<FolderPicker value="" onChange={onChange} />);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
await waitFor(() => expect(onChange).toHaveBeenCalledWith("/Users/example/Code"));
expect(invoke).toHaveBeenCalledWith("pick_directory", undefined);
expect(mockedListDirectory).not.toHaveBeenCalled();
expect(screen.queryByTestId("folder-picker-popover")).toBeNull();
});
it("builds Windows breadcrumbs with a virtual root and native drive paths", async () => {
mockedListDirectory
.mockResolvedValueOnce(
listing(VIRTUAL_ROOT, false, [{ name: DRIVE_E_ROOT, path: DRIVE_E_ROOT }]),
)
.mockResolvedValueOnce(listing("E:\\Projects\\Kandev", true))
.mockResolvedValueOnce(listing(VIRTUAL_ROOT, false));
render(<FolderPicker value="" onChange={vi.fn()} />);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
fireEvent.click(await screen.findByRole(BUTTON_ROLE, { name: DRIVE_E_ROOT }));
await screen.findByRole(BUTTON_ROLE, { name: "Kandev" });
expect(screen.getByRole(BUTTON_ROLE, { name: VIRTUAL_ROOT })).toBeTruthy();
expect(screen.getByRole(BUTTON_ROLE, { name: DRIVE_E_ROOT })).toBeTruthy();
expect(screen.getByRole(BUTTON_ROLE, { name: "Projects" })).toBeTruthy();
fireEvent.click(screen.getByRole(BUTTON_ROLE, { name: VIRTUAL_ROOT }));
expect(mockedListDirectory).toHaveBeenLastCalledWith(VIRTUAL_ROOT);
});
it("clears a stale selectable listing when navigation fails", async () => {
mockedListDirectory
.mockResolvedValueOnce(listing("C:\\", true, [{ name: "Denied", path: "C:\\Denied" }]))
.mockRejectedValueOnce(new Error("failed to list directory"));
render(<FolderPicker value="" onChange={vi.fn()} />);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
fireEvent.click(await screen.findByRole(BUTTON_ROLE, { name: "Denied" }));
await screen.findByTestId("folder-picker-error");
await waitFor(() =>
expect(screen.getByTestId("folder-picker-choose").hasAttribute("disabled")).toBe(true),
);
expect(screen.queryByRole(BUTTON_ROLE, { name: "C:\\" })).toBeNull();
});
it("ignores an old success after a value change starts a successor load", async () => {
const oldLoad = deferred<DirectoryListing>();
const successorLoad = deferred<DirectoryListing>();
mockedListDirectory
.mockReturnValueOnce(oldLoad.promise)
.mockReturnValueOnce(successorLoad.promise);
const { rerender } = render(<FolderPicker value="" onChange={vi.fn()} />);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
await waitFor(() => expect(mockedListDirectory).toHaveBeenCalledWith(""));
rerender(<FolderPicker value={SUCCESS_PATH} onChange={vi.fn()} />);
await waitFor(() => expect(mockedListDirectory).toHaveBeenCalledWith(SUCCESS_PATH));
await act(async () => successorLoad.resolve(listing(SUCCESS_PATH, true)));
await waitFor(() =>
expect(screen.getAllByRole(BUTTON_ROLE, { name: SUCCESS_NAME })).toHaveLength(2),
);
await act(async () => oldLoad.resolve(listing("C:\\Stale", true)));
expect(screen.queryByRole(BUTTON_ROLE, { name: "Stale" })).toBeNull();
expect(screen.getAllByRole(BUTTON_ROLE, { name: SUCCESS_NAME })).toHaveLength(2);
});
it("ignores an old error after close and reopen completes a successor load", async () => {
const oldLoad = deferred<DirectoryListing>();
const successorLoad = deferred<DirectoryListing>();
mockedListDirectory
.mockReturnValueOnce(oldLoad.promise)
.mockReturnValueOnce(successorLoad.promise);
render(<FolderPicker value="" onChange={vi.fn()} />);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
await waitFor(() => expect(mockedListDirectory).toHaveBeenCalledTimes(1));
act(() => setPopoverOpen?.(false));
act(() => setPopoverOpen?.(true));
await waitFor(() => expect(mockedListDirectory).toHaveBeenCalledTimes(2));
await act(async () => successorLoad.resolve(listing(SUCCESS_PATH, true)));
await screen.findByRole(BUTTON_ROLE, { name: SUCCESS_NAME });
await act(async () => oldLoad.reject(new Error("stale failure")));
expect(screen.queryByTestId("folder-picker-error")).toBeNull();
expect(screen.getByRole(BUTTON_ROLE, { name: SUCCESS_NAME })).toBeTruthy();
expect(screen.getByTestId("folder-picker-choose").hasAttribute("disabled")).toBe(false);
});
});
function listing(
path: string,
choosable: boolean,
entries: DirectoryListing["entries"] = [],
): DirectoryListing {
return { path, parent: "", entries, choosable };
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}