diff --git a/apps/web/components/task/file-browser-parts.tsx b/apps/web/components/task/file-browser-parts.tsx index da056ae8e2..9746438c89 100644 --- a/apps/web/components/task/file-browser-parts.tsx +++ b/apps/web/components/task/file-browser-parts.tsx @@ -120,9 +120,12 @@ function TreeNodeChildren({ props, depth }: { props: TreeNodeItemProps; depth: n onCancel={onCancelCreate} /> )} - {node.children?.map((child) => ( - - ))} + {node.children && + [...node.children] + .sort(compareTreeNodes) + .map((child) => ( + + ))} ); } @@ -142,6 +145,9 @@ export function TreeNodeItem(props: TreeNodeItemProps) { const rowContent = (
{ + const task = await apiClient.createTaskWithAgent( + seedData.workspaceId, + title, + seedData.agentProfileId, + { + description: "/e2e:simple-message", + workflow_id: seedData.workflowId, + workflow_step_id: seedData.startStepId, + repository_ids: [seedData.repositoryId], + }, + ); + + await testPage.goto(`/t/${task.id}`); + + const session = new SessionPage(testPage); + await session.waitForLoad(); + await expect(session.idleInput()).toBeVisible({ timeout: 30_000 }); + + return session; +} + +test.describe("File tree sorting", () => { + test.describe.configure({ retries: 1 }); + + test("directories appear before files inside nested folders, including dotfiles", async ({ + testPage, + apiClient, + seedData, + backend, + }) => { + // Seed a nested folder containing a mix of dirs (incl. a dot-dir) and files + // (incl. dotfiles). If the sort is wrong, dotfiles will appear above .vscode. + const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo"); + const subDir = path.join(repoDir, "sub"); + + fs.mkdirSync(path.join(subDir, "adir"), { recursive: true }); + fs.writeFileSync(path.join(subDir, "adir", "keep.txt"), "x\n"); + fs.mkdirSync(path.join(subDir, ".vscode"), { recursive: true }); + fs.writeFileSync(path.join(subDir, ".vscode", "settings.json"), "{}\n"); + fs.writeFileSync(path.join(subDir, ".gitignore"), "node_modules\n"); + fs.writeFileSync(path.join(subDir, ".golangci.yaml"), "linters:\n"); + fs.writeFileSync(path.join(subDir, "zfile.txt"), "z\n"); + + const session = await seedSimpleTask(testPage, apiClient, seedData, "File Tree Sorting"); + + await session.clickTab("Files"); + await expect(session.files).toBeVisible({ timeout: 5_000 }); + + // Expand the sub folder + const subRow = session.files.locator('[data-testid="file-tree-node"][data-path="sub"]'); + await expect(subRow).toBeVisible({ timeout: 10_000 }); + await subRow.click(); + + // Wait until all expected children are rendered + const childSelector = (p: string) => + session.files.locator(`[data-testid="file-tree-node"][data-path="${p}"]`); + + await expect(childSelector("sub/adir")).toBeVisible({ timeout: 10_000 }); + await expect(childSelector("sub/.vscode")).toBeVisible({ timeout: 10_000 }); + await expect(childSelector("sub/.gitignore")).toBeVisible({ timeout: 10_000 }); + await expect(childSelector("sub/.golangci.yaml")).toBeVisible({ timeout: 10_000 }); + await expect(childSelector("sub/zfile.txt")).toBeVisible({ timeout: 10_000 }); + + // Read the rendered order of sub's direct children + const childPaths = await session.files + .locator('[data-testid="file-tree-node"]') + .evaluateAll((els) => + els + .map((el) => ({ + path: el.getAttribute("data-path") ?? "", + isDir: el.getAttribute("data-is-dir") === "true", + })) + .filter((n) => n.path.startsWith("sub/") && !n.path.slice("sub/".length).includes("/")), + ); + + // Directories must come before files. Within each group, order follows + // the comparator's localeCompare (dot-prefixed names sort before letters). + const dirs = childPaths.filter((n) => n.isDir).map((n) => n.path); + const files = childPaths.filter((n) => !n.isDir).map((n) => n.path); + + expect(dirs).toEqual(["sub/.vscode", "sub/adir"]); + expect(files).toEqual(["sub/.gitignore", "sub/.golangci.yaml", "sub/zfile.txt"]); + }); +});