test: file-tree load more
This commit is contained in:
parent
03aa1a7e7e
commit
e9a2b42d6d
4 changed files with 129 additions and 15 deletions
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -170,4 +170,4 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
void main();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue