mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat(preload): add file.list, file.pickFolder, file.pickFile convenience methods
This commit is contained in:
@@ -1,10 +1,22 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import { Sidebar } from '@/components/sidebar/Sidebar';
|
||||
import { useFileStore } from '@/stores/file-store';
|
||||
import { useEditorStore } from '@/stores/editor-store';
|
||||
|
||||
vi.mock('@/lib/ipc', () => ({
|
||||
ipc: {
|
||||
file: {
|
||||
pickFolder: vi.fn(),
|
||||
list: vi.fn(),
|
||||
pickFile: vi.fn(),
|
||||
read: vi.fn(),
|
||||
write: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Sidebar', () => {
|
||||
beforeEach(() => {
|
||||
useFileStore.setState({ tree: null, rootPath: null, expanded: new Set(), openTabs: [], activeTabId: null });
|
||||
@@ -45,4 +57,36 @@ describe('Sidebar', () => {
|
||||
expect(screen.getByText('Outline')).toBeInTheDocument();
|
||||
expect(screen.getByText('README.md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Open Folder" button when no folder is open', () => {
|
||||
useFileStore.setState({ tree: null, rootPath: null });
|
||||
|
||||
render(
|
||||
<ThemeProvider defaultTheme="dark" attribute="class">
|
||||
<Sidebar />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/no folder opened/i)).toBeInTheDocument();
|
||||
const btn = screen.getByRole('button', { name: /open folder/i });
|
||||
expect(btn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking "Open Folder" button calls openFolderDialog', async () => {
|
||||
const { ipc } = await import('@/lib/ipc');
|
||||
const spy = vi.spyOn(useFileStore.getState(), 'openFolderDialog');
|
||||
|
||||
useFileStore.setState({ tree: null, rootPath: null });
|
||||
|
||||
render(
|
||||
<ThemeProvider defaultTheme="dark" attribute="class">
|
||||
<Sidebar />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const btn = screen.getByRole('button', { name: /open folder/i });
|
||||
fireEvent.click(btn);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@ vi.mock('@/lib/ipc', () => ({
|
||||
file: {
|
||||
list: vi.fn(),
|
||||
read: vi.fn(),
|
||||
pickFolder: vi.fn(),
|
||||
pickFile: vi.fn(),
|
||||
write: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -16,6 +19,9 @@ import { ipc } from '@/lib/ipc';
|
||||
|
||||
const fakeList = ipc.file.list as ReturnType<typeof vi.fn>;
|
||||
const fakeRead = ipc.file.read as ReturnType<typeof vi.fn>;
|
||||
const fakePickFolder = ipc.file.pickFolder as ReturnType<typeof vi.fn>;
|
||||
const fakePickFile = ipc.file.pickFile as ReturnType<typeof vi.fn>;
|
||||
const fakeWrite = ipc.file.write as ReturnType<typeof vi.fn>;
|
||||
|
||||
function fakeFileEntry(name: string, isDirectory: boolean): FileEntry {
|
||||
return { name, path: `/root/${name}`, isDirectory };
|
||||
@@ -33,6 +39,9 @@ describe('useFileStore', () => {
|
||||
useEditorStore.setState({ buffers: new Map(), activeId: null });
|
||||
fakeList.mockClear();
|
||||
fakeRead.mockClear();
|
||||
fakePickFolder.mockClear();
|
||||
fakePickFile.mockClear();
|
||||
fakeWrite.mockClear();
|
||||
});
|
||||
|
||||
// --- openFolder ---
|
||||
@@ -302,4 +311,89 @@ describe('useFileStore', () => {
|
||||
const tabs = useFileStore.getState().openTabs;
|
||||
expect(tabs.map((t) => t.title)).toEqual(['c.md', 'a.md', 'b.md']);
|
||||
});
|
||||
|
||||
// --- openFolderDialog ---
|
||||
|
||||
it('openFolderDialog calls pickFolder, then openFolder on the returned path', async () => {
|
||||
fakePickFolder.mockResolvedValue({ ok: true, data: '/projects/myapp' });
|
||||
fakeList.mockResolvedValue({
|
||||
ok: true,
|
||||
data: [fakeFileEntry('README.md', false)],
|
||||
});
|
||||
|
||||
await useFileStore.getState().openFolderDialog();
|
||||
|
||||
expect(fakePickFolder).toHaveBeenCalled();
|
||||
expect(fakeList).toHaveBeenCalledWith('/projects/myapp');
|
||||
expect(useFileStore.getState().rootPath).toBe('/projects/myapp');
|
||||
});
|
||||
|
||||
it('openFolderDialog on cancelled (null) is a no-op', async () => {
|
||||
fakePickFolder.mockResolvedValue({ ok: true, data: null });
|
||||
|
||||
await useFileStore.getState().openFolderDialog();
|
||||
|
||||
expect(fakeList).not.toHaveBeenCalled();
|
||||
expect(useFileStore.getState().tree).toBeNull();
|
||||
});
|
||||
|
||||
// --- openFileDialog ---
|
||||
|
||||
it('openFileDialog calls pickFile, then openFile on the returned path', async () => {
|
||||
fakePickFile.mockResolvedValue({ ok: true, data: '/root/README.md' });
|
||||
fakeRead.mockResolvedValue({ ok: true, data: '# hello' });
|
||||
|
||||
await useFileStore.getState().openFileDialog();
|
||||
|
||||
expect(fakePickFile).toHaveBeenCalled();
|
||||
expect(fakeRead).toHaveBeenCalledWith('/root/README.md');
|
||||
const tabs = useFileStore.getState().openTabs;
|
||||
expect(tabs).toHaveLength(1);
|
||||
expect(tabs[0].path).toBe('/root/README.md');
|
||||
});
|
||||
|
||||
it('openFileDialog on cancelled (null) is a no-op', async () => {
|
||||
fakePickFile.mockResolvedValue({ ok: true, data: null });
|
||||
|
||||
await useFileStore.getState().openFileDialog();
|
||||
|
||||
expect(fakeRead).not.toHaveBeenCalled();
|
||||
expect(useFileStore.getState().openTabs).toHaveLength(0);
|
||||
});
|
||||
|
||||
// --- saveActiveBuffer ---
|
||||
|
||||
it('saveActiveBuffer with no active tab returns false', async () => {
|
||||
const result = await useFileStore.getState().saveActiveBuffer();
|
||||
expect(result).toBe(false);
|
||||
expect(fakeWrite).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saveActiveBuffer with active buffer calls write, then marks saved and clean, returns true', async () => {
|
||||
fakeRead.mockResolvedValue({ ok: true, data: '# content' });
|
||||
await useFileStore.getState().openFile('/root/doc.md');
|
||||
fakeWrite.mockResolvedValue({ ok: true, data: undefined });
|
||||
|
||||
const result = await useFileStore.getState().saveActiveBuffer();
|
||||
|
||||
expect(fakeWrite).toHaveBeenCalledWith('/root/doc.md', '# content');
|
||||
expect(useEditorStore.getState().buffers.get('/root/doc.md')!.dirty).toBe(false);
|
||||
expect(useFileStore.getState().openTabs[0].dirty).toBe(false);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('saveActiveBuffer on write failure returns false and does NOT mark clean', async () => {
|
||||
fakeRead.mockResolvedValue({ ok: true, data: '# content' });
|
||||
await useFileStore.getState().openFile('/root/doc.md');
|
||||
// Mark the buffer dirty and sync the tab dirty flag before attempting save
|
||||
useEditorStore.getState().updateContent('/root/doc.md', '# modified');
|
||||
useFileStore.getState().markTabDirty('/root/doc.md');
|
||||
fakeWrite.mockResolvedValue({ ok: false, error: { code: 'EIO', message: 'write error' } });
|
||||
|
||||
const result = await useFileStore.getState().saveActiveBuffer();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(useEditorStore.getState().buffers.get('/root/doc.md')!.dirty).toBe(true);
|
||||
expect(useFileStore.getState().openTabs[0].dirty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user