mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
500 lines
13 KiB
TypeScript
500 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { renderHook, act } from '@testing-library/react';
|
|
import { useFileList } from './useFileList';
|
|
import * as fileApi from '@/api/file';
|
|
import type { FileNode } from '@/types/models';
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/api/file');
|
|
|
|
// Mock workspace context
|
|
const mockWorkspaceData: {
|
|
currentWorkspace: { id: number; name: string } | null;
|
|
loading: boolean;
|
|
} = {
|
|
currentWorkspace: {
|
|
id: 1,
|
|
name: 'test-workspace',
|
|
},
|
|
loading: false,
|
|
};
|
|
|
|
vi.mock('../contexts/WorkspaceDataContext', () => ({
|
|
useWorkspaceData: () => mockWorkspaceData,
|
|
}));
|
|
|
|
// Mock file data
|
|
const mockFiles: FileNode[] = [
|
|
{
|
|
id: '1',
|
|
name: 'README.md',
|
|
path: 'README.md',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'docs',
|
|
path: 'docs',
|
|
children: [
|
|
{
|
|
id: '3',
|
|
name: 'guide.md',
|
|
path: 'docs/guide.md',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: '4',
|
|
name: 'notes.md',
|
|
path: 'notes.md',
|
|
},
|
|
];
|
|
|
|
describe('useFileList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset workspace data to defaults
|
|
mockWorkspaceData.currentWorkspace = {
|
|
id: 1,
|
|
name: 'test-workspace',
|
|
};
|
|
mockWorkspaceData.loading = false;
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('initial state', () => {
|
|
it('starts with empty files array', () => {
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(typeof result.current.loadFileList).toBe('function');
|
|
});
|
|
|
|
it('provides loadFileList function', () => {
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
expect(typeof result.current.loadFileList).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('loadFileList', () => {
|
|
it('loads files successfully', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(mockFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
|
});
|
|
|
|
it('handles empty file list', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue([]);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
|
});
|
|
|
|
it('handles API errors gracefully', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
const consoleSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
mockListFiles.mockRejectedValue(new Error('API Error'));
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'Failed to load file list:',
|
|
expect.any(Error)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('does not load when no workspace is available', async () => {
|
|
mockWorkspaceData.currentWorkspace = null;
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(fileApi.listFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not load when workspace is loading', async () => {
|
|
mockWorkspaceData.loading = true;
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(fileApi.listFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('can be called multiple times', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles
|
|
.mockResolvedValueOnce(mockFiles[0] ? [mockFiles[0]] : [])
|
|
.mockResolvedValueOnce(mockFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
// First call
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([mockFiles[0]]);
|
|
|
|
// Second call
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
expect(mockListFiles).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('handles concurrent calls gracefully', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(mockFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
// Make multiple concurrent calls
|
|
await Promise.all([
|
|
result.current.loadFileList(),
|
|
result.current.loadFileList(),
|
|
result.current.loadFileList(),
|
|
]);
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
expect(mockListFiles).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('workspace dependency', () => {
|
|
it('uses correct workspace name for API calls', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(mockFiles);
|
|
|
|
const { result, rerender } = renderHook(() => useFileList());
|
|
|
|
// Load with initial workspace
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
|
|
|
// Change workspace
|
|
mockWorkspaceData.currentWorkspace = {
|
|
id: 2,
|
|
name: 'different-workspace',
|
|
};
|
|
|
|
rerender();
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(mockListFiles).toHaveBeenCalledWith('different-workspace');
|
|
});
|
|
|
|
it('handles workspace becoming null after successful load', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(mockFiles);
|
|
|
|
const { result, rerender } = renderHook(() => useFileList());
|
|
|
|
// Load files with workspace
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
|
|
// Remove workspace
|
|
mockWorkspaceData.currentWorkspace = null;
|
|
rerender();
|
|
|
|
// Try to load again
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
// Files should remain from previous load, but no new API call
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
expect(mockListFiles).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('handles workspace loading state changes', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(mockFiles);
|
|
|
|
const { result, rerender } = renderHook(() => useFileList());
|
|
|
|
// Start with loading workspace
|
|
mockWorkspaceData.loading = true;
|
|
rerender();
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(mockListFiles).not.toHaveBeenCalled();
|
|
|
|
// Workspace finishes loading
|
|
mockWorkspaceData.loading = false;
|
|
rerender();
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
|
});
|
|
});
|
|
|
|
describe('file data handling', () => {
|
|
it('handles complex file tree structure', async () => {
|
|
const complexFiles: FileNode[] = [
|
|
{
|
|
id: '1',
|
|
name: 'root.md',
|
|
path: 'root.md',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'folder1',
|
|
path: 'folder1',
|
|
children: [
|
|
{
|
|
id: '3',
|
|
name: 'subfolder',
|
|
path: 'folder1/subfolder',
|
|
children: [
|
|
{
|
|
id: '4',
|
|
name: 'deep.md',
|
|
path: 'folder1/subfolder/deep.md',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: '5',
|
|
name: 'file1.md',
|
|
path: 'folder1/file1.md',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(complexFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(complexFiles);
|
|
});
|
|
|
|
it('handles large file lists efficiently', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
|
|
// Create a large file list
|
|
const largeFileList: FileNode[] = Array.from(
|
|
{ length: 1000 },
|
|
(_, i) => ({
|
|
id: `file-${i}`,
|
|
name: `file-${i}.md`,
|
|
path: `folder/file-${i}.md`,
|
|
})
|
|
);
|
|
|
|
mockListFiles.mockResolvedValue(largeFileList);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(largeFileList);
|
|
expect(result.current.files).toHaveLength(1000);
|
|
});
|
|
|
|
it('handles files with special characters', async () => {
|
|
const specialFiles: FileNode[] = [
|
|
{
|
|
id: '1',
|
|
name: 'file with spaces.md',
|
|
path: 'file with spaces.md',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'special-chars_123.md',
|
|
path: 'special-chars_123.md',
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'unicode-文档.md',
|
|
path: 'unicode-文档.md',
|
|
},
|
|
];
|
|
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(specialFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(specialFiles);
|
|
});
|
|
|
|
it('handles files without children property', async () => {
|
|
const filesWithoutChildren: FileNode[] = [
|
|
{
|
|
id: '1',
|
|
name: 'simple.md',
|
|
path: 'simple.md',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'another.md',
|
|
path: 'another.md',
|
|
},
|
|
];
|
|
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
mockListFiles.mockResolvedValue(filesWithoutChildren);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(filesWithoutChildren);
|
|
});
|
|
});
|
|
|
|
describe('hook interface stability', () => {
|
|
it('loadFileList function is stable across re-renders', () => {
|
|
const { result, rerender } = renderHook(() => useFileList());
|
|
|
|
const initialLoadFunction = result.current.loadFileList;
|
|
|
|
rerender();
|
|
|
|
expect(result.current.loadFileList).toBe(initialLoadFunction);
|
|
});
|
|
|
|
it('returns consistent interface', () => {
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
expect(Array.isArray(result.current.files)).toBe(true);
|
|
expect(typeof result.current.loadFileList).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('error recovery', () => {
|
|
it('recovers from API errors on subsequent calls', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
const consoleSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
// First call fails
|
|
mockListFiles.mockRejectedValueOnce(new Error('First error'));
|
|
// Second call succeeds
|
|
mockListFiles.mockResolvedValueOnce(mockFiles);
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
// First call - should fail
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
|
|
// Second call - should succeed
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('maintains previous data after error', async () => {
|
|
const mockListFiles = vi.mocked(fileApi.listFiles);
|
|
const consoleSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
// First call succeeds
|
|
mockListFiles.mockResolvedValueOnce(mockFiles);
|
|
// Second call fails
|
|
mockListFiles.mockRejectedValueOnce(new Error('Second error'));
|
|
|
|
const { result } = renderHook(() => useFileList());
|
|
|
|
// First call - should succeed
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual(mockFiles);
|
|
|
|
// Second call - should fail but maintain previous data
|
|
await act(async () => {
|
|
await result.current.loadFileList();
|
|
});
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|