Files
lemma/app/src/hooks/useLastOpenedFile.test.ts

377 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useLastOpenedFile } from './useLastOpenedFile';
import * as fileApi from '@/api/file';
// Mock dependencies
vi.mock('@/api/file');
// Mock the workspace context
const mockWorkspaceData: {
currentWorkspace: { id: number; name: string } | null;
} = {
currentWorkspace: {
id: 1,
name: 'test-workspace',
},
};
vi.mock('../contexts/WorkspaceDataContext', () => ({
useWorkspaceData: () => mockWorkspaceData,
}));
describe('useLastOpenedFile', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset workspace data to defaults
mockWorkspaceData.currentWorkspace = {
id: 1,
name: 'test-workspace',
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('loadLastOpenedFile', () => {
it('loads last opened file successfully', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
mockGetLastOpenedFile.mockResolvedValue('documents/readme.md');
const { result } = renderHook(() => useLastOpenedFile());
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBe('documents/readme.md');
expect(mockGetLastOpenedFile).toHaveBeenCalledWith('test-workspace');
});
it('returns null for empty response', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
mockGetLastOpenedFile.mockResolvedValue('');
const { result } = renderHook(() => useLastOpenedFile());
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBeNull();
expect(mockGetLastOpenedFile).toHaveBeenCalledWith('test-workspace');
});
it('handles API errors gracefully', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockGetLastOpenedFile.mockRejectedValue(new Error('API Error'));
const { result } = renderHook(() => useLastOpenedFile());
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to load last opened file:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('returns null when no workspace is available', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useLastOpenedFile());
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBeNull();
expect(fileApi.getLastOpenedFile).not.toHaveBeenCalled();
});
it('handles different file path formats', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
// Test various file path formats
const testCases = [
'simple.md',
'folder/file.md',
'deep/nested/path/document.md',
'file with spaces.md',
'special-chars_123.md',
];
const { result } = renderHook(() => useLastOpenedFile());
for (const testPath of testCases) {
mockGetLastOpenedFile.mockResolvedValueOnce(testPath);
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBe(testPath);
}
expect(mockGetLastOpenedFile).toHaveBeenCalledTimes(testCases.length);
});
});
describe('saveLastOpenedFile', () => {
it('saves last opened file successfully', async () => {
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
const { result } = renderHook(() => useLastOpenedFile());
await act(async () => {
await result.current.saveLastOpenedFile('notes/todo.md');
});
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'test-workspace',
'notes/todo.md'
);
});
it('handles API errors gracefully', async () => {
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockUpdateLastOpenedFile.mockRejectedValue(new Error('Save Error'));
const { result } = renderHook(() => useLastOpenedFile());
await act(async () => {
await result.current.saveLastOpenedFile('error.md');
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to save last opened file:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('does nothing when no workspace is available', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useLastOpenedFile());
await act(async () => {
await result.current.saveLastOpenedFile('test.md');
});
expect(fileApi.updateLastOpenedFile).not.toHaveBeenCalled();
});
it('does nothing when file path is empty', async () => {
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
const { result } = renderHook(() => useLastOpenedFile());
await act(async () => {
await result.current.saveLastOpenedFile('');
});
expect(mockUpdateLastOpenedFile).not.toHaveBeenCalledWith(
'test-workspace',
''
);
});
it('handles different file path formats', async () => {
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
const testCases = [
'simple.md',
'folder/file.md',
'deep/nested/path/document.md',
'file with spaces.md',
'special-chars_123.md',
'unicode-文件.md',
];
const { result } = renderHook(() => useLastOpenedFile());
for (const testPath of testCases) {
await act(async () => {
await result.current.saveLastOpenedFile(testPath);
});
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'test-workspace',
testPath
);
}
expect(mockUpdateLastOpenedFile).toHaveBeenCalledTimes(testCases.length);
});
});
describe('workspace dependency', () => {
it('handles workspace changes correctly', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockGetLastOpenedFile.mockResolvedValue('file.md');
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
const { result, rerender } = renderHook(() => useLastOpenedFile());
// Test with initial workspace
await act(async () => {
await result.current.loadLastOpenedFile();
await result.current.saveLastOpenedFile('test.md');
});
expect(mockGetLastOpenedFile).toHaveBeenCalledWith('test-workspace');
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'test-workspace',
'test.md'
);
// Change workspace
mockWorkspaceData.currentWorkspace = {
id: 2,
name: 'different-workspace',
};
rerender();
await act(async () => {
await result.current.loadLastOpenedFile();
await result.current.saveLastOpenedFile('other.md');
});
expect(mockGetLastOpenedFile).toHaveBeenCalledWith('different-workspace');
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'different-workspace',
'other.md'
);
});
it('handles workspace becoming null', async () => {
const { result, rerender } = renderHook(() => useLastOpenedFile());
// Start with workspace
expect(mockWorkspaceData.currentWorkspace).not.toBeNull();
// Remove workspace
mockWorkspaceData.currentWorkspace = null;
rerender();
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
await result.current.saveLastOpenedFile('test.md');
});
expect(lastFile).toBeNull();
expect(fileApi.getLastOpenedFile).not.toHaveBeenCalled();
expect(fileApi.updateLastOpenedFile).not.toHaveBeenCalled();
});
});
describe('hook interface', () => {
it('returns correct function interface', () => {
const { result } = renderHook(() => useLastOpenedFile());
expect(typeof result.current.loadLastOpenedFile).toBe('function');
expect(typeof result.current.saveLastOpenedFile).toBe('function');
});
it('functions are stable across re-renders', () => {
const { result, rerender } = renderHook(() => useLastOpenedFile());
const initialHandlers = {
loadLastOpenedFile: result.current.loadLastOpenedFile,
saveLastOpenedFile: result.current.saveLastOpenedFile,
};
rerender();
expect(result.current.loadLastOpenedFile).toBe(
initialHandlers.loadLastOpenedFile
);
expect(result.current.saveLastOpenedFile).toBe(
initialHandlers.saveLastOpenedFile
);
});
});
describe('integration scenarios', () => {
it('handles load after save', async () => {
const mockGetLastOpenedFile = vi.mocked(fileApi.getLastOpenedFile);
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
mockGetLastOpenedFile.mockResolvedValue('saved-file.md');
const { result } = renderHook(() => useLastOpenedFile());
// Save a file
await act(async () => {
await result.current.saveLastOpenedFile('saved-file.md');
});
// Load the last opened file
let lastFile: string | null = '';
await act(async () => {
lastFile = await result.current.loadLastOpenedFile();
});
expect(lastFile).toBe('saved-file.md');
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'test-workspace',
'saved-file.md'
);
expect(mockGetLastOpenedFile).toHaveBeenCalledWith('test-workspace');
});
it('handles multiple rapid saves', async () => {
const mockUpdateLastOpenedFile = vi.mocked(fileApi.updateLastOpenedFile);
mockUpdateLastOpenedFile.mockResolvedValue(undefined);
const { result } = renderHook(() => useLastOpenedFile());
const filePaths = ['file1.md', 'file2.md', 'file3.md'];
// Rapidly save multiple files
await act(async () => {
await Promise.all(
filePaths.map((path) => result.current.saveLastOpenedFile(path))
);
});
expect(mockUpdateLastOpenedFile).toHaveBeenCalledTimes(3);
filePaths.forEach((path) => {
expect(mockUpdateLastOpenedFile).toHaveBeenCalledWith(
'test-workspace',
path
);
});
});
});
});