From e9a2b42d6d941f2e8dcaaebae427f8f7dd690557 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 31 Jan 2026 14:59:06 +0100 Subject: [PATCH] test: file-tree load more --- .../components/__test__/file-tree.test.tsx | 108 ++++++++++++++++++ app/client/components/file-tree.tsx | 24 +++- app/server/modules/volumes/volume.service.ts | 10 +- scripts/create-test-files.ts | 2 +- 4 files changed, 129 insertions(+), 15 deletions(-) diff --git a/app/client/components/__test__/file-tree.test.tsx b/app/client/components/__test__/file-tree.test.tsx index 7589fe63..f2aa43a6 100644 --- a/app/client/components/__test__/file-tree.test.tsx +++ b/app/client/components/__test__/file-tree.test.tsx @@ -3,6 +3,114 @@ import { expect, test, describe } from "bun:test"; import { render, screen, fireEvent } from "@testing-library/react"; 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( + ({ hasMore: true, isLoadingMore: false })} + />, + ); + + expect(screen.getByText("Load more files")).toBeTruthy(); + }); + + test("does not show load more button when hasMore is false", () => { + render( + ({ 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( + { + 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( + ({ 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( + { + 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( + ({ hasMore: true, isLoadingMore: false })} + />, + ); + + expect(screen.queryByText("Load more files")).toBeNull(); + }); +}); + describe("FileTree Selection Logic", () => { const testFiles: FileEntry[] = [ { name: "root", path: "/root", type: "folder" }, diff --git a/app/client/components/file-tree.tsx b/app/client/components/file-tree.tsx index 5731faa0..9a39ca7b 100644 --- a/app/client/components/file-tree.tsx +++ b/app/client/components/file-tree.tsx @@ -8,7 +8,15 @@ * 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 { cn } from "~/client/lib/utils"; 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 const folderPaginationMap = useMemo(() => { const map = new Map(); - + for (let i = 0; i < filteredFileList.length; i++) { const item = filteredFileList[i]; const parentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/")) || "/"; - + if (parentPath !== "/") { const pagination = getFolderPagination?.(parentPath); if (pagination?.hasMore && !collapsedFolders.has(parentPath)) { @@ -317,7 +325,7 @@ export const FileTree = memo((props: Props) => { } } } - + return map; }, [filteredFileList, getFolderPagination, collapsedFolders]); @@ -539,7 +547,13 @@ const LoadMoreButton = memo(({ depth, onClick, isLoading }: LoadMoreButtonProps) : } + icon={ + isLoading ? ( + + ) : ( + + ) + } onClick={onClick} > {isLoading ? "Loading more..." : "Load more files"} diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 34651328..fb561b11 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -297,14 +297,6 @@ const checkHealth = async (idOrShortId: string | number) => { const DEFAULT_PAGE_SIZE = 500; const MAX_PAGE_SIZE = 500; -interface DirEntry { - name: string; - path: string; - type: "directory" | "file"; - size?: number; - modifiedAt?: number; -} - const listFiles = async ( idOrShortId: string | number, subPath?: string, @@ -361,7 +353,7 @@ const listFiles = async ( return { name: dirent.name, path: `/${relativePath}`, - type: dirent.isDirectory() ? "directory" : "file", + type: dirent.isDirectory() ? ("directory" as const) : ("file" as const), size: dirent.isFile() ? stats.size : undefined, modifiedAt: stats.mtimeMs, }; diff --git a/scripts/create-test-files.ts b/scripts/create-test-files.ts index 27e194df..e5c15713 100755 --- a/scripts/create-test-files.ts +++ b/scripts/create-test-files.ts @@ -170,4 +170,4 @@ async function main(): Promise { } } -main(); +void main();