test: file-tree load more

This commit is contained in:
Nicolas Meienberger 2026-01-31 14:59:06 +01:00
parent 03aa1a7e7e
commit e9a2b42d6d
4 changed files with 129 additions and 15 deletions

View file

@ -3,6 +3,114 @@ import { expect, test, describe } from "bun:test";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { FileTree, type FileEntry } from "../file-tree"; import { FileTree, type FileEntry } from "../file-tree";
describe("FileTree Pagination", () => {
const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "file1", path: "/root/file1", type: "file" },
{ name: "file2", path: "/root/file2", type: "file" },
];
test("shows load more button when hasMore is true", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={() => ({ hasMore: true, isLoadingMore: false })}
/>,
);
expect(screen.getByText("Load more files")).toBeTruthy();
});
test("does not show load more button when hasMore is false", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={() => ({ hasMore: false, isLoadingMore: false })}
/>,
);
expect(screen.queryByText("Load more files")).toBeNull();
});
test("calls onLoadMore with folder path when load more button is clicked", () => {
let loadMoreCalled = false;
let loadMorePath = "";
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={(path) => {
if (path === "/root") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
onLoadMore={(path) => {
loadMoreCalled = true;
loadMorePath = path;
}}
/>,
);
const loadMoreButton = screen.getByText("Load more files");
fireEvent.click(loadMoreButton);
expect(loadMoreCalled).toBe(true);
expect(loadMorePath).toBe("/root");
});
test("shows loading state when isLoadingMore is true", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={() => ({ hasMore: true, isLoadingMore: true })}
/>,
);
expect(screen.getByText("Loading more...")).toBeTruthy();
});
test("load more button appears for nested folders with hasMore", () => {
const nestedFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "child", path: "/root/child", type: "folder" },
{ name: "file1", path: "/root/child/file1", type: "file" },
];
render(
<FileTree
files={nestedFiles}
expandedFolders={new Set(["/root", "/root/child"])}
getFolderPagination={(path) => {
if (path === "/root/child") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
onLoadMore={() => {}}
/>,
);
expect(screen.getByText("Load more files")).toBeTruthy();
});
test("load more button does not appear when folder is collapsed", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set([])}
getFolderPagination={() => ({ hasMore: true, isLoadingMore: false })}
/>,
);
expect(screen.queryByText("Load more files")).toBeNull();
});
});
describe("FileTree Selection Logic", () => { describe("FileTree Selection Logic", () => {
const testFiles: FileEntry[] = [ const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" }, { name: "root", path: "/root", type: "folder" },

View file

@ -8,7 +8,15 @@
* Original source: https://github.com/stackblitz/bolt.new * Original source: https://github.com/stackblitz/bolt.new
*/ */
import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2, MoreHorizontal } from "lucide-react"; import {
ChevronDown,
ChevronRight,
File as FileIcon,
Folder as FolderIcon,
FolderOpen,
Loader2,
MoreHorizontal,
} from "lucide-react";
import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { Checkbox } from "~/client/components/ui/checkbox"; import { Checkbox } from "~/client/components/ui/checkbox";
@ -304,11 +312,11 @@ export const FileTree = memo((props: Props) => {
// Build a map of folder paths that need pagination to their last child's index // Build a map of folder paths that need pagination to their last child's index
const folderPaginationMap = useMemo(() => { const folderPaginationMap = useMemo(() => {
const map = new Map<string, number>(); const map = new Map<string, number>();
for (let i = 0; i < filteredFileList.length; i++) { for (let i = 0; i < filteredFileList.length; i++) {
const item = filteredFileList[i]; const item = filteredFileList[i];
const parentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/")) || "/"; const parentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/")) || "/";
if (parentPath !== "/") { if (parentPath !== "/") {
const pagination = getFolderPagination?.(parentPath); const pagination = getFolderPagination?.(parentPath);
if (pagination?.hasMore && !collapsedFolders.has(parentPath)) { if (pagination?.hasMore && !collapsedFolders.has(parentPath)) {
@ -317,7 +325,7 @@ export const FileTree = memo((props: Props) => {
} }
} }
} }
return map; return map;
}, [filteredFileList, getFolderPagination, collapsedFolders]); }, [filteredFileList, getFolderPagination, collapsedFolders]);
@ -539,7 +547,13 @@ const LoadMoreButton = memo(({ depth, onClick, isLoading }: LoadMoreButtonProps)
<NodeButton <NodeButton
depth={depth} depth={depth}
className="text-muted-foreground hover:bg-accent/50 cursor-pointer" className="text-muted-foreground hover:bg-accent/50 cursor-pointer"
icon={isLoading ? <Loader2 className="w-4 h-4 shrink-0 animate-spin" /> : <MoreHorizontal className="w-4 h-4 shrink-0" />} icon={
isLoading ? (
<Loader2 className="w-4 h-4 shrink-0 animate-spin" />
) : (
<MoreHorizontal className="w-4 h-4 shrink-0" />
)
}
onClick={onClick} onClick={onClick}
> >
<span className="text-xs">{isLoading ? "Loading more..." : "Load more files"}</span> <span className="text-xs">{isLoading ? "Loading more..." : "Load more files"}</span>

View file

@ -297,14 +297,6 @@ const checkHealth = async (idOrShortId: string | number) => {
const DEFAULT_PAGE_SIZE = 500; const DEFAULT_PAGE_SIZE = 500;
const MAX_PAGE_SIZE = 500; const MAX_PAGE_SIZE = 500;
interface DirEntry {
name: string;
path: string;
type: "directory" | "file";
size?: number;
modifiedAt?: number;
}
const listFiles = async ( const listFiles = async (
idOrShortId: string | number, idOrShortId: string | number,
subPath?: string, subPath?: string,
@ -361,7 +353,7 @@ const listFiles = async (
return { return {
name: dirent.name, name: dirent.name,
path: `/${relativePath}`, path: `/${relativePath}`,
type: dirent.isDirectory() ? "directory" : "file", type: dirent.isDirectory() ? ("directory" as const) : ("file" as const),
size: dirent.isFile() ? stats.size : undefined, size: dirent.isFile() ? stats.size : undefined,
modifiedAt: stats.mtimeMs, modifiedAt: stats.mtimeMs,
}; };

View file

@ -170,4 +170,4 @@ async function main(): Promise<void> {
} }
} }
main(); void main();