kimi-code / apps /vscode /test /webview /useFilePicker.test.tsx
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5710dd0 verified
Raw
History Blame Contribute Delete
9.33 kB
import { act, renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useFilePicker } from '@/components/inputarea/hooks/useFilePicker';
import { useChatStore } from '@/stores';
const getProjectFiles = vi.fn();
vi.mock('@/services', () => ({
bridge: {
getProjectFiles: (...args: unknown[]) => getProjectFiles(...args),
},
}));
const noop = () => {};
const at = (query: string) => ({ trigger: '@' as const, start: 0, query });
type Token = ReturnType<typeof at> | null;
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
beforeEach(() => {
getProjectFiles.mockReset();
getProjectFiles.mockResolvedValue([]);
useChatStore.setState({ isStreaming: false, draftMedia: [] });
});
describe('enabled gating', () => {
it('does not search without an active token', async () => {
renderHook(() => useFilePicker(null, noop, noop, noop), { wrapper: createWrapper() });
await new Promise((r) => setTimeout(r, 150));
expect(getProjectFiles).not.toHaveBeenCalled();
});
});
describe('search', () => {
it('searches with the current query and returns results', async () => {
getProjectFiles.mockResolvedValue([{ name: 'app.ts', path: 'src/app.ts', isDirectory: false }]);
const { result } = renderHook(() => useFilePicker(at('app'), noop, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(1); });
expect(result.current.fileItems[0]).toMatchObject({ name: 'app.ts', path: 'src/app.ts' });
});
it('caps the displayed items at 50', async () => {
getProjectFiles.mockResolvedValue(
Array.from({ length: 60 }, (_, i) => ({ name: `f${i}.ts`, path: `f${i}.ts`, isDirectory: false })),
);
const { result } = renderHook(() => useFilePicker(at('f'), noop, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(50); });
});
it('debounces rapid query changes into a single request after 100ms', async () => {
const { rerender } = renderHook(({ token }) => useFilePicker(token, noop, noop, noop), {
initialProps: { token: at('') as Token },
wrapper: createWrapper(),
});
await waitFor(() => { expect(getProjectFiles).toHaveBeenCalledTimes(1); });
getProjectFiles.mockClear();
rerender({ token: at('a') });
rerender({ token: at('ap') });
rerender({ token: at('app') });
expect(getProjectFiles).not.toHaveBeenCalled();
await waitFor(() => { expect(getProjectFiles).toHaveBeenCalledTimes(1); });
expect(getProjectFiles).toHaveBeenCalledWith({ query: 'app' });
});
});
describe('media option', () => {
it('shows the media option only for an empty query', async () => {
const { result, rerender } = renderHook(({ token }) => useFilePicker(token, noop, noop, noop), {
initialProps: { token: at('') as Token },
wrapper: createWrapper(),
});
expect(result.current.showMediaOption).toBe(true);
expect(result.current.fileMenuHeaderCount).toBe(1);
rerender({ token: at('a') });
expect(result.current.showMediaOption).toBe(false);
expect(result.current.fileMenuHeaderCount).toBe(0);
});
it('hides the media option when media cannot be added', () => {
useChatStore.setState({ isStreaming: true });
const { result } = renderHook(() => useFilePicker(at(''), noop, noop, noop), { wrapper: createWrapper() });
expect(result.current.showMediaOption).toBe(false);
expect(result.current.fileMenuHeaderCount).toBe(0);
});
});
describe('keyboard navigation', () => {
const key = (result: { current: ReturnType<typeof useFilePicker> }, k: string) => {
act(() => {
result.current.handleFileMenuKey({ key: k, preventDefault: vi.fn() } as unknown as React.KeyboardEvent);
});
};
it('moves selectedIndex within the list bounds with ArrowDown and ArrowUp', async () => {
getProjectFiles.mockResolvedValue([
{ name: 'a.ts', path: 'a.ts', isDirectory: false },
{ name: 'b.ts', path: 'b.ts', isDirectory: false },
]);
const { result } = renderHook(() => useFilePicker(at('a'), noop, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(2); });
const maxIndex = result.current.fileMenuHeaderCount + result.current.fileItems.length - 1;
expect(result.current.selectedIndex).toBe(0);
key(result, 'ArrowUp');
expect(result.current.selectedIndex).toBe(0);
key(result, 'ArrowDown');
key(result, 'ArrowDown');
key(result, 'ArrowDown');
expect(result.current.selectedIndex).toBe(maxIndex);
key(result, 'ArrowUp');
expect(result.current.selectedIndex).toBe(maxIndex - 1);
});
it('lets Enter fall through when there is no selectable entry', async () => {
getProjectFiles.mockResolvedValue([]);
const { result } = renderHook(() => useFilePicker(at('zzz'), noop, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.isLoading).toBe(false); });
let handled = true;
act(() => {
handled = result.current.handleFileMenuKey({ key: 'Enter', preventDefault: vi.fn() } as unknown as React.KeyboardEvent);
});
expect(handled).toBe(false);
});
it('clamps the selection when fresh results shrink the list', async () => {
getProjectFiles.mockResolvedValue([
{ name: 'a.ts', path: 'a.ts', isDirectory: false },
{ name: 'b.ts', path: 'b.ts', isDirectory: false },
]);
const { result, rerender } = renderHook(({ token }) => useFilePicker(token, noop, noop, noop), {
initialProps: { token: at('a') as Token },
wrapper: createWrapper(),
});
await waitFor(() => { expect(result.current.fileItems).toHaveLength(2); });
getProjectFiles.mockResolvedValue([{ name: 'app.ts', path: 'app.ts', isDirectory: false }]);
rerender({ token: at('ap') });
act(() => { result.current.setSelectedIndex(1); });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(1); });
expect(result.current.selectedIndex).toBe(0);
});
it('calls onPickMedia when Enter selects the media option', async () => {
const onPickMedia = vi.fn();
const { result } = renderHook(() => useFilePicker(at(''), noop, onPickMedia, noop), { wrapper: createWrapper() });
expect(result.current.selectedIndex).toBe(0);
key(result, 'Enter');
expect(onPickMedia).toHaveBeenCalledTimes(1);
});
it('ignores confirmation while results are stale for the current query', async () => {
getProjectFiles.mockResolvedValue([{ name: 'a.ts', path: 'src/a.ts', isDirectory: false }]);
const onInsertFile = vi.fn();
const { result, rerender } = renderHook(({ token }) => useFilePicker(token, onInsertFile, noop, noop), {
initialProps: { token: at('a') as Token },
wrapper: createWrapper(),
});
await waitFor(() => { expect(result.current.fileItems).toHaveLength(1); });
let resolveNext: (value: unknown) => void = noop;
getProjectFiles.mockImplementation(() => new Promise((resolve) => { resolveNext = resolve; }));
rerender({ token: at('ap') });
expect(result.current.isStale).toBe(true);
key(result, 'Enter');
act(() => { result.current.handleSelectItem(result.current.fileItems[0]!); });
expect(onInsertFile).not.toHaveBeenCalled();
await waitFor(() => { expect(getProjectFiles).toHaveBeenCalledWith({ query: 'ap' }); });
act(() => { resolveNext([{ name: 'app.ts', path: 'src/app.ts', isDirectory: false }]); });
await waitFor(() => { expect(result.current.fileItems[0]?.name).toBe('app.ts'); });
expect(result.current.isStale).toBe(false);
key(result, 'Enter');
expect(onInsertFile).toHaveBeenCalledWith('src/app.ts');
});
it('calls onInsertFile when Enter selects a file', async () => {
getProjectFiles.mockResolvedValue([{ name: 'a.ts', path: 'src/a.ts', isDirectory: false }]);
const onInsertFile = vi.fn();
const { result } = renderHook(() => useFilePicker(at('a'), onInsertFile, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(1); });
act(() => {
result.current.setSelectedIndex(result.current.fileMenuHeaderCount);
});
key(result, 'Enter');
expect(onInsertFile).toHaveBeenCalledWith('src/a.ts');
});
it('calls onInsertFile with the directory path when Enter selects a directory', async () => {
getProjectFiles.mockResolvedValue([{ name: 'src', path: 'src', isDirectory: true }]);
const onInsertFile = vi.fn();
const { result } = renderHook(() => useFilePicker(at('sr'), onInsertFile, noop, noop), { wrapper: createWrapper() });
await waitFor(() => { expect(result.current.fileItems).toHaveLength(1); });
act(() => {
result.current.setSelectedIndex(result.current.fileMenuHeaderCount);
});
key(result, 'Enter');
expect(onInsertFile).toHaveBeenCalledWith('src');
});
});