diff --git a/packages/app/e2e/file-explorer-collapse.spec.ts b/packages/app/e2e/file-explorer-collapse.spec.ts new file mode 100644 index 000000000..97fcd9e30 --- /dev/null +++ b/packages/app/e2e/file-explorer-collapse.spec.ts @@ -0,0 +1,61 @@ +import { test } from "./fixtures"; +import { + collapseFolder, + expandFolder, + expectExplorerEntryHidden, + expectExplorerEntryVisible, + expectFileTabOpen, + openFileExplorer, + openFileFromExplorer, +} from "./helpers/file-explorer"; +import { gotoWorkspace } from "./helpers/launcher"; +import { createTempGitRepo } from "./helpers/workspace"; +import { + connectWorkspaceSetupClient, + type WorkspaceSetupDaemonClient, +} from "./helpers/workspace-setup"; + +let tempRepo: { path: string; cleanup: () => Promise }; +let workspaceId: string; +let seedClient: WorkspaceSetupDaemonClient; + +test.beforeAll(async () => { + tempRepo = await createTempGitRepo("file-explorer-collapse-", { + files: [ + { path: "assets/logo.png", content: "image bytes for explorer e2e\n" }, + { path: "docs/guide.md", content: "# Guide\n" }, + ], + }); + seedClient = await connectWorkspaceSetupClient(); + const result = await seedClient.openProject(tempRepo.path); + if (!result.workspace) { + throw new Error(result.error ?? "Failed to seed workspace"); + } + workspaceId = String(result.workspace.id); +}); + +test.afterAll(async () => { + await seedClient?.close(); + await tempRepo?.cleanup(); +}); + +test.describe("File explorer collapse", () => { + test("collapses an opened image file parent folder and still expands other folders", async ({ + page, + }) => { + await gotoWorkspace(page, workspaceId); + await openFileExplorer(page); + + await expandFolder(page, "assets"); + await expectExplorerEntryVisible(page, "logo.png"); + + await openFileFromExplorer(page, "logo.png"); + await expectFileTabOpen(page, "assets/logo.png"); + + await collapseFolder(page, "assets"); + await expectExplorerEntryHidden(page, "logo.png"); + + await expandFolder(page, "docs"); + await expectExplorerEntryVisible(page, "guide.md"); + }); +}); diff --git a/packages/app/e2e/helpers/file-explorer.ts b/packages/app/e2e/helpers/file-explorer.ts new file mode 100644 index 000000000..0d63f34ba --- /dev/null +++ b/packages/app/e2e/helpers/file-explorer.ts @@ -0,0 +1,41 @@ +import { expect, type Page } from "@playwright/test"; + +function fileExplorerTree(page: Page) { + return page.getByTestId("file-explorer-tree-scroll"); +} + +function fileExplorerEntry(page: Page, name: string) { + return fileExplorerTree(page).getByText(name, { exact: true }).first(); +} + +export async function openFileExplorer(page: Page): Promise { + await page.getByRole("button", { name: "Open explorer" }).first().click(); + await page.getByTestId("explorer-tab-files").click(); + await expect(fileExplorerTree(page)).toBeVisible({ timeout: 30_000 }); +} + +export async function expandFolder(page: Page, folderName: string): Promise { + await fileExplorerEntry(page, folderName).click(); +} + +export async function collapseFolder(page: Page, folderName: string): Promise { + await fileExplorerEntry(page, folderName).click(); +} + +export async function openFileFromExplorer(page: Page, fileName: string): Promise { + await fileExplorerEntry(page, fileName).click(); +} + +export async function expectExplorerEntryVisible(page: Page, name: string): Promise { + await expect(fileExplorerEntry(page, name)).toBeVisible({ timeout: 30_000 }); +} + +export async function expectExplorerEntryHidden(page: Page, name: string): Promise { + await expect(fileExplorerEntry(page, name)).toBeHidden({ timeout: 30_000 }); +} + +export async function expectFileTabOpen(page: Page, filePath: string): Promise { + await expect(page.getByTestId(`workspace-tab-file_${filePath}`).first()).toBeVisible({ + timeout: 30_000, + }); +} diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 9575eb891..7b09137ca 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -195,35 +195,6 @@ export function FileExplorerPane({ }); }, [hasWorkspaceScope, requestDirectoryListing, workspaceStateKey]); - // Expand ancestor directories when a file is selected (e.g., from an inline path click) - useEffect(() => { - if (!selectedEntryPath || !workspaceStateKey) { - return; - } - const parentDir = getParentDirectory(selectedEntryPath); - const ancestors = getAncestorDirectories(parentDir); - const newPaths = ancestors.filter((path) => !expandedPaths.has(path)); - if (newPaths.length === 0) { - return; - } - setExpandedPathsForWorkspace(workspaceStateKey, [...Array.from(expandedPaths), ...newPaths]); - newPaths.forEach((path) => { - if (!directories.has(path)) { - void requestDirectoryListing(path, { - recordHistory: false, - setCurrentPath: false, - }); - } - }); - }, [ - directories, - workspaceStateKey, - expandedPaths, - requestDirectoryListing, - selectedEntryPath, - setExpandedPathsForWorkspace, - ]); - const handleToggleDirectory = useCallback( (entry: ExplorerEntry) => { if (!workspaceStateKey) { @@ -682,35 +653,6 @@ function buildTreeRows({ return rows; } -function getParentDirectory(path: string): string { - const normalized = path.replace(/\/+$/, ""); - if (!normalized || normalized === ".") { - return "."; - } - const lastSlash = normalized.lastIndexOf("/"); - if (lastSlash === -1) { - return "."; - } - const dir = normalized.slice(0, lastSlash); - return dir.length > 0 ? dir : "."; -} - -function getAncestorDirectories(directory: string): string[] { - const trimmed = directory.replace(/^\.\/+/, "").replace(/\/+$/, ""); - if (!trimmed || trimmed === ".") { - return ["."]; - } - - const parts = trimmed.split("/").filter(Boolean); - const ancestors: string[] = ["."]; - let acc = ""; - for (const part of parts) { - acc = acc ? `${acc}/${part}` : part; - ancestors.push(acc); - } - return ancestors; -} - function getErrorRecoveryPath(state: AgentFileExplorerState | undefined): string { if (!state) { return ".";