mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
377 lines
11 KiB
TypeScript
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
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|