codev / desktop /src /components /chat /FileSearchMenu.test.tsx
chenbhao's picture
feat: desktop
1f21206
Raw
History Blame
6.15 kB
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { FileSearchMenu } from './FileSearchMenu'
import { ApiError } from '../../api/client'
import { filesystemApi } from '../../api/filesystem'
import { useSettingsStore } from '../../stores/settingsStore'
vi.mock('../../api/filesystem', () => ({
filesystemApi: {
browse: vi.fn(),
search: vi.fn(),
},
}))
describe('FileSearchMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
useSettingsStore.setState({ locale: 'en' })
})
it('shows an explicit error when directory browsing is denied', async () => {
vi.mocked(filesystemApi.browse).mockRejectedValueOnce(
new ApiError(403, { error: 'Access denied: path outside allowed directory' }),
)
render(
<FileSearchMenu
cwd="/private/tmp"
onSelect={() => {}}
/>,
)
expect(await screen.findByText('Cannot access this directory')).toBeInTheDocument()
expect(screen.queryByText('No files in this directory')).not.toBeInTheDocument()
})
it('renders returned files when browsing succeeds', async () => {
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
currentPath: '/tmp',
parentPath: '/',
entries: [
{ name: 'preview.png', path: '/tmp/preview.png', isDirectory: false },
],
})
render(
<FileSearchMenu
cwd="/tmp"
onSelect={() => {}}
/>,
)
await waitFor(() => {
expect(screen.getByText('preview.png')).toBeInTheDocument()
})
})
it('selects directories from search results and keeps a separate drill-in action', async () => {
const onSelect = vi.fn()
const onNavigate = vi.fn()
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
currentPath: '/repo',
parentPath: '/',
query: 'backend',
entries: [
{ name: 'backend', path: '/repo/backend', relativePath: 'backend', isDirectory: true },
],
})
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
currentPath: '/repo/backend',
parentPath: '/repo',
entries: [
{ name: 'src', path: '/repo/backend/src', isDirectory: true },
],
})
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
currentPath: '/repo/backend/src',
parentPath: '/repo/backend',
entries: [
{ name: 'commands', path: '/repo/backend/src/commands', isDirectory: true },
],
})
render(
<FileSearchMenu
cwd="/repo"
filter="backend"
onSelect={onSelect}
onNavigate={onNavigate}
/>,
)
fireEvent.click(await screen.findByText('backend/'))
expect(onSelect).toHaveBeenCalledWith('/repo/backend', 'backend', true)
expect(onNavigate).not.toHaveBeenCalled()
fireEvent.click(screen.getByLabelText('Open folder'))
expect(onNavigate).toHaveBeenCalledWith('backend/')
await waitFor(() => {
expect(filesystemApi.browse).toHaveBeenCalledWith('/repo/backend', { includeFiles: true })
})
fireEvent.click(await screen.findByText('src'))
expect(onSelect).toHaveBeenLastCalledWith('/repo/backend/src', 'backend/src', true)
fireEvent.click(screen.getByLabelText('Open folder'))
expect(onNavigate).toHaveBeenLastCalledWith('backend/src/')
await waitFor(() => {
expect(filesystemApi.browse).toHaveBeenCalledWith('/repo/backend/src', { includeFiles: true })
})
})
it('passes nested relative file paths when selecting a file', async () => {
const onSelect = vi.fn()
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
currentPath: '/repo',
parentPath: '/',
query: 'pictactic',
entries: [
{ name: 'pictactic', path: '/repo/backend/src/pictactic', relativePath: 'backend/src/pictactic', isDirectory: false },
],
})
render(
<FileSearchMenu
cwd="/repo"
filter="backend/src/pictactic"
onSelect={onSelect}
/>,
)
fireEvent.click(await screen.findByText('backend/src/pictactic'))
expect(onSelect).toHaveBeenCalledWith('/repo/backend/src/pictactic', 'backend/src/pictactic', false)
})
it('renders search results as insertable paths instead of repeated basenames', async () => {
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
currentPath: '/repo',
parentPath: '/',
query: 'src',
entries: [
{ name: 'src', path: '/repo/src', relativePath: 'src', isDirectory: true },
{ name: 'hooks', path: '/repo/src/hooks', relativePath: 'src/hooks', isDirectory: true },
{ name: 'src', path: '/repo/desktop/src', relativePath: 'desktop/src', isDirectory: true },
],
})
render(
<FileSearchMenu
cwd="/repo"
filter="src"
onSelect={() => {}}
/>,
)
expect(await screen.findByText('src/')).toBeInTheDocument()
expect(screen.getByText('src/hooks/')).toBeInTheDocument()
expect(screen.getByText('desktop/src/')).toBeInTheDocument()
})
it('uses the resolved home root for typed folder filters when no workspace is selected', async () => {
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
currentPath: '/Users/nanmi',
parentPath: '/',
query: 'workspace',
entries: [
{ name: 'workspace', path: '/Users/nanmi/workspace', relativePath: 'workspace', isDirectory: true },
],
})
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
currentPath: '/Users/nanmi/workspace',
parentPath: '/Users/nanmi',
entries: [],
})
const { rerender } = render(
<FileSearchMenu
cwd=""
filter="workspace"
onSelect={() => {}}
/>,
)
expect(await screen.findByText('workspace/')).toBeInTheDocument()
rerender(
<FileSearchMenu
cwd=""
filter="workspace/"
onSelect={() => {}}
/>,
)
await waitFor(() => {
expect(filesystemApi.browse).toHaveBeenCalledWith('/Users/nanmi/workspace', { includeFiles: true })
})
})
})