diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index 08476c69..82388547 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -71,7 +71,7 @@ interface FileTreeProps { isShowSourceCode: boolean; } -const FileTree: React.FC = ({ +export const FileTree: React.FC = ({ node, level = 0, selectedFile, @@ -105,29 +105,33 @@ const FileTree: React.FC = ({ onSelectFile(fileInfo); } }} - className={`text-primary flex w-full items-center justify-start rounded-xl bg-fill-fill-transparent p-2 text-left text-sm backdrop-blur-lg transition-colors hover:bg-fill-fill-transparent-active ${ + className={`text-primary flex w-full items-center justify-start gap-2 rounded-xl bg-fill-fill-transparent p-2 text-left text-sm backdrop-blur-lg transition-colors hover:bg-fill-fill-transparent-active ${ selectedFile?.path === child.path ? 'bg-fill-fill-transparent-active' : '' }`} > - {child.isFolder && ( - + {child.isFolder ? ( + {isExpanded ? ( ) : ( )} + ) : ( + )} - {!child.isFolder && } {child.isFolder ? ( - + ) : child.icon ? ( - + ) : ( - + )} { + const onToggleFolder = vi.fn(); + const onSelectFile = vi.fn(); + + const nodeWithFolderAndFile = { + name: '', + path: '', + children: [ + { name: 'src', path: '/proj/src', isFolder: true, children: [] }, + { name: 'readme.md', path: '/proj/readme.md', isFolder: false }, + ], + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders folder and file rows', () => { + render( + + ); + expect(screen.getByRole('button', { name: /src/i })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /readme\.md/i }) + ).toBeInTheDocument(); + }); + it('uses consistent first-column box (h-4 w-4) for folder and file rows for alignment', () => { + const { container } = render( + + ); + const buttons = container.querySelectorAll('button'); + expect(buttons.length).toBe(2); + buttons.forEach((btn) => { + const firstCol = btn.querySelector('[class*="h-4"][class*="w-4"]'); + expect(firstCol).toBeInTheDocument(); + }); + }); + it('uses gap-2 on row for consistent spacing between chevron, icon, and label', () => { + const { container } = render( + + ); + const buttons = container.querySelectorAll('button'); + buttons.forEach((btn) => { + expect(btn.className).toMatch(/gap-2/); + }); + }); + it('file row first column has aria-hidden for accessibility', () => { + render( + + ); + const fileButton = screen.getByRole('button', { name: /readme\.md/i }); + const spacer = fileButton.querySelector('[aria-hidden="true"]'); + expect(spacer).toBeInTheDocument(); + }); + + it('calls onToggleFolder when folder row is clicked', async () => { + render( + + ); + await userEvent.click(screen.getByRole('button', { name: /src/i })); + expect(onToggleFolder).toHaveBeenCalledWith('/proj/src'); + }); + + it('calls onSelectFile when file row is clicked', async () => { + render( + + ); + await userEvent.click(screen.getByRole('button', { name: /readme\.md/i })); + expect(onSelectFile).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'readme.md', + path: '/proj/readme.md', + isFolder: false, + }) + ); + }); + + it('returns null when node has no children', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); +});