2026-03-31 11:19:31 +02:00
|
|
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
|
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
|
import { FolderTree } from './FolderTree'
|
|
|
|
|
import type { FolderNode, SidebarSelection } from '../types'
|
|
|
|
|
|
|
|
|
|
const mockFolders: FolderNode[] = [
|
|
|
|
|
{
|
|
|
|
|
name: 'projects',
|
|
|
|
|
path: 'projects',
|
|
|
|
|
children: [
|
|
|
|
|
{ name: 'laputa', path: 'projects/laputa', children: [] },
|
|
|
|
|
{ name: 'portfolio', path: 'projects/portfolio', children: [] },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{ name: 'areas', path: 'areas', children: [] },
|
|
|
|
|
{ name: 'journal', path: 'journal', children: [] },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const defaultSelection: SidebarSelection = { kind: 'filter', filter: 'all' }
|
|
|
|
|
|
|
|
|
|
describe('FolderTree', () => {
|
|
|
|
|
it('renders nothing when folders is empty', () => {
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<FolderTree folders={[]} selection={defaultSelection} onSelect={vi.fn()} />,
|
|
|
|
|
)
|
|
|
|
|
expect(container.firstChild).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders FOLDERS header and top-level folders', () => {
|
|
|
|
|
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={vi.fn()} />)
|
|
|
|
|
expect(screen.getByText('FOLDERS')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('projects')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('areas')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('journal')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 17:16:56 +02:00
|
|
|
it('expands children when clicking the folder chevron', () => {
|
2026-03-31 11:19:31 +02:00
|
|
|
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={vi.fn()} />)
|
|
|
|
|
expect(screen.queryByText('laputa')).not.toBeInTheDocument()
|
2026-04-22 17:16:56 +02:00
|
|
|
fireEvent.click(screen.getByLabelText('Expand projects'))
|
|
|
|
|
expect(screen.getByText('laputa')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('portfolio')).toBeInTheDocument()
|
2026-03-31 11:19:31 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-22 17:16:56 +02:00
|
|
|
it('calls onSelect with folder kind when clicking a folder row', () => {
|
2026-03-31 11:19:31 +02:00
|
|
|
const onSelect = vi.fn()
|
|
|
|
|
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={onSelect} />)
|
2026-04-22 17:16:56 +02:00
|
|
|
fireEvent.click(screen.getByTestId('folder-row:projects'))
|
2026-03-31 11:19:31 +02:00
|
|
|
expect(onSelect).toHaveBeenCalledWith({ kind: 'folder', path: 'projects' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 17:16:56 +02:00
|
|
|
it('collapses section when clicking the FOLDERS header', () => {
|
2026-03-31 11:19:31 +02:00
|
|
|
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={vi.fn()} />)
|
|
|
|
|
expect(screen.getByText('projects')).toBeInTheDocument()
|
|
|
|
|
fireEvent.click(screen.getByText('FOLDERS'))
|
|
|
|
|
expect(screen.queryByText('projects')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 17:16:56 +02:00
|
|
|
it('highlights the selected folder row', () => {
|
|
|
|
|
const selection: SidebarSelection = { kind: 'folder', path: 'areas' }
|
|
|
|
|
render(<FolderTree folders={mockFolders} selection={selection} onSelect={vi.fn()} />)
|
|
|
|
|
expect(screen.getByTestId('folder-row:areas').className).toContain('text-primary')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens the create-folder input from the header action', () => {
|
|
|
|
|
render(
|
|
|
|
|
<FolderTree
|
|
|
|
|
folders={mockFolders}
|
|
|
|
|
selection={defaultSelection}
|
|
|
|
|
onSelect={vi.fn()}
|
|
|
|
|
onCreateFolder={vi.fn().mockResolvedValue(true)}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
fireEvent.click(screen.getByTestId('create-folder-btn'))
|
|
|
|
|
expect(screen.getByTestId('new-folder-input')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('starts rename on folder double-click', () => {
|
|
|
|
|
const onStartRenameFolder = vi.fn()
|
|
|
|
|
render(
|
|
|
|
|
<FolderTree
|
|
|
|
|
folders={mockFolders}
|
|
|
|
|
selection={defaultSelection}
|
|
|
|
|
onSelect={vi.fn()}
|
|
|
|
|
onRenameFolder={vi.fn().mockResolvedValue(true)}
|
|
|
|
|
onStartRenameFolder={onStartRenameFolder}
|
|
|
|
|
onCancelRenameFolder={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
fireEvent.doubleClick(screen.getByTestId('folder-row:areas'))
|
|
|
|
|
expect(onStartRenameFolder).toHaveBeenCalledWith('areas')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 22:49:16 +02:00
|
|
|
it('shows inline rename and delete actions for folders', () => {
|
|
|
|
|
const onDeleteFolder = vi.fn()
|
|
|
|
|
const onStartRenameFolder = vi.fn()
|
|
|
|
|
const onSelect = vi.fn()
|
|
|
|
|
render(
|
|
|
|
|
<FolderTree
|
|
|
|
|
folders={mockFolders}
|
|
|
|
|
selection={defaultSelection}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
onDeleteFolder={onDeleteFolder}
|
|
|
|
|
onRenameFolder={vi.fn().mockResolvedValue(true)}
|
|
|
|
|
onStartRenameFolder={onStartRenameFolder}
|
|
|
|
|
onCancelRenameFolder={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByTestId('rename-folder-btn:projects'))
|
|
|
|
|
fireEvent.click(screen.getByTestId('delete-folder-btn:projects'))
|
|
|
|
|
|
|
|
|
|
expect(onSelect).toHaveBeenNthCalledWith(1, { kind: 'folder', path: 'projects' })
|
|
|
|
|
expect(onStartRenameFolder).toHaveBeenCalledWith('projects')
|
|
|
|
|
expect(onSelect).toHaveBeenNthCalledWith(2, { kind: 'folder', path: 'projects' })
|
|
|
|
|
expect(onDeleteFolder).toHaveBeenCalledWith('projects')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 17:16:56 +02:00
|
|
|
it('shows the rename input when a folder is being renamed', () => {
|
|
|
|
|
render(
|
|
|
|
|
<FolderTree
|
|
|
|
|
folders={mockFolders}
|
|
|
|
|
selection={{ kind: 'folder', path: 'areas' }}
|
|
|
|
|
onSelect={vi.fn()}
|
|
|
|
|
onRenameFolder={vi.fn().mockResolvedValue(true)}
|
|
|
|
|
renamingFolderPath="areas"
|
|
|
|
|
onCancelRenameFolder={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
expect(screen.getByTestId('rename-folder-input')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens a context menu with a delete action on right-click', () => {
|
|
|
|
|
const onDeleteFolder = vi.fn()
|
|
|
|
|
render(
|
|
|
|
|
<FolderTree
|
|
|
|
|
folders={mockFolders}
|
|
|
|
|
selection={defaultSelection}
|
|
|
|
|
onSelect={vi.fn()}
|
|
|
|
|
onDeleteFolder={onDeleteFolder}
|
|
|
|
|
onStartRenameFolder={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
fireEvent.contextMenu(screen.getByText('projects'))
|
|
|
|
|
expect(screen.getByTestId('folder-context-menu')).toBeInTheDocument()
|
|
|
|
|
fireEvent.click(screen.getByTestId('delete-folder-menu-item'))
|
|
|
|
|
expect(onDeleteFolder).toHaveBeenCalledWith('projects')
|
2026-03-31 11:19:31 +02:00
|
|
|
})
|
|
|
|
|
})
|