From 338669f2db21e9e2244c4ebf6c23a48b58ba8701 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 22:59:54 +0530 Subject: [PATCH] feat(renderer): file-store toasts on save/open/folder --- src/renderer/stores/file-store.ts | 18 +++- tests/unit/stores/file-store-toasts.test.ts | 100 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/unit/stores/file-store-toasts.test.ts diff --git a/src/renderer/stores/file-store.ts b/src/renderer/stores/file-store.ts index c2bf7d7..55463b7 100644 --- a/src/renderer/stores/file-store.ts +++ b/src/renderer/stores/file-store.ts @@ -3,6 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { enableMapSet } from 'immer'; import { ipc } from '@/lib/ipc'; +import { toast } from '@/lib/toast'; import { useEditorStore } from '@/stores/editor-store'; import type { FileEntry } from '@/types/ipc'; @@ -79,7 +80,10 @@ export const useFileStore = create()( openFolder: async (path) => { const result = await ipc.file.list(path); - if (!result.ok) return; + if (!result.ok) { + toast.error(`Failed to open folder: ${result.error.message}`); + return; + } const children: FileNode[] = result.data!.map(entryToNode); @@ -144,7 +148,10 @@ export const useFileStore = create()( } const result = await ipc.file.read(filePath); - if (!result.ok) return; + if (!result.ok) { + toast.error(`Failed to open file: ${result.error.message}`); + return; + } const content = result.data!; const title = filePath.split('/').pop() ?? filePath; @@ -217,10 +224,15 @@ export const useFileStore = create()( if (!buffer) return false; const writeResult = await ipc.file.write(activeTabId, buffer.content); - if (!writeResult.ok) return false; + if (!writeResult.ok) { + toast.error(`Failed to save: ${writeResult.error.message}`); + return false; + } useEditorStore.getState().markSaved(activeTabId); useFileStore.getState().markTabClean(activeTabId); + const title = useFileStore.getState().openTabs.find(t => t.id === activeTabId)?.title ?? activeTabId.split('/').pop() ?? activeTabId; + toast.success(`Saved ${title}`); return true; }, })), diff --git a/tests/unit/stores/file-store-toasts.test.ts b/tests/unit/stores/file-store-toasts.test.ts new file mode 100644 index 0000000..bfbbb73 --- /dev/null +++ b/tests/unit/stores/file-store-toasts.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useEditorStore } from '@/stores/editor-store'; +import { useFileStore } from '@/stores/file-store'; + +vi.mock('@/lib/ipc', () => ({ + ipc: { + file: { + list: vi.fn(), + read: vi.fn(), + pickFolder: vi.fn(), + pickFile: vi.fn(), + write: vi.fn(), + }, + }, +})); + +vi.mock('@/lib/toast', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warning: vi.fn(), + promise: vi.fn(), + dismiss: vi.fn(), + }, +})); + +import { ipc } from '@/lib/ipc'; +import { toast } from '@/lib/toast'; + +const fakeRead = ipc.file.read as ReturnType; +const fakeWrite = ipc.file.write as ReturnType; +const fakeList = ipc.file.list as ReturnType; + +describe('file-store toasts', () => { + beforeEach(() => { + localStorage.clear(); + vi.clearAllMocks(); + useFileStore.setState({ + tree: null, + rootPath: null, + expanded: new Set(), + openTabs: [], + activeTabId: null, + } as any); + useEditorStore.setState({ buffers: new Map(), activeId: null } as any); + }); + + describe('saveActiveBuffer', () => { + it('calls toast.success when save succeeds', async () => { + fakeWrite.mockResolvedValue({ ok: true, data: undefined }); + useFileStore.setState({ + activeTabId: '/test.md', + openTabs: [{ id: '/test.md', path: '/test.md', title: 'test.md', dirty: false }], + } as any); + useEditorStore.setState({ + buffers: new Map([['/test.md', { id: '/test.md', path: '/test.md', content: '# hi', dirty: true }]]), + activeId: '/test.md', + } as any); + + const result = await useFileStore.getState().saveActiveBuffer(); + expect(result).toBe(true); + expect(toast.success).toHaveBeenCalledWith('Saved test.md'); + }); + + it('calls toast.error when save fails', async () => { + fakeWrite.mockResolvedValue({ ok: false, error: { code: 'EACCES', message: 'Permission denied' } }); + useFileStore.setState({ + activeTabId: '/test.md', + openTabs: [{ id: '/test.md', path: '/test.md', title: 'test.md', dirty: false }], + } as any); + useEditorStore.setState({ + buffers: new Map([['/test.md', { id: '/test.md', path: '/test.md', content: '# hi', dirty: true }]]), + activeId: '/test.md', + } as any); + + const result = await useFileStore.getState().saveActiveBuffer(); + expect(result).toBe(false); + expect(toast.error).toHaveBeenCalledWith('Failed to save: Permission denied'); + }); + }); + + describe('openFile', () => { + it('calls toast.error when IPC read fails', async () => { + fakeRead.mockResolvedValue({ ok: false, error: { code: 'ENOENT', message: 'No such file' } }); + + await useFileStore.getState().openFile('/missing.md'); + expect(toast.error).toHaveBeenCalledWith('Failed to open file: No such file'); + }); + }); + + describe('openFolder', () => { + it('calls toast.error when IPC list fails', async () => { + fakeList.mockResolvedValue({ ok: false, error: { code: 'EACCES', message: 'Permission denied' } }); + + await useFileStore.getState().openFolder('/forbidden'); + expect(toast.error).toHaveBeenCalledWith('Failed to open folder: Permission denied'); + }); + }); +}); \ No newline at end of file