diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index cdcf3a45..66d53c50 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -716,6 +716,47 @@ describe('filterEntries — trash', () => { }) }) +describe('NoteList — modified indicators', () => { + it('shows modified indicator dot for entries in modifiedFiles', () => { + const modifiedFiles = [ + { path: mockEntries[0].path, relativePath: 'project/26q1-laputa-app.md', status: 'modified' as const }, + ] + render( + + ) + const indicators = screen.getAllByTestId('modified-indicator') + expect(indicators).toHaveLength(1) + // The indicator's parent div contains the title text + const noteRow = indicators[0].closest('[data-testid="modified-indicator"]')!.parentElement!.parentElement! + expect(noteRow.textContent).toContain('Build Laputa App') + }) + + it('does not show modified indicator when modifiedFiles is empty', () => { + render( + + ) + expect(screen.queryByTestId('modified-indicator')).not.toBeInTheDocument() + }) + + it('shows multiple modified indicators for multiple modified files', () => { + const modifiedFiles = [ + { path: mockEntries[0].path, relativePath: 'project/26q1-laputa-app.md', status: 'modified' as const }, + { path: mockEntries[1].path, relativePath: 'note/facebook-ads-strategy.md', status: 'modified' as const }, + ] + render( + + ) + expect(screen.getAllByTestId('modified-indicator')).toHaveLength(2) + }) + + it('does not show modified indicator when modifiedFiles prop is undefined', () => { + render( + + ) + expect(screen.queryByTestId('modified-indicator')).not.toBeInTheDocument() + }) +}) + describe('NoteList — trash view', () => { const trashSelection: SidebarSelection = { kind: 'filter', filter: 'trash' } diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index d5d2db18..06664716 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -128,6 +128,22 @@ describe('StatusBar', () => { expect(screen.getByText('Connect GitHub repo')).toBeInTheDocument() }) + it('shows modified count when modifiedCount is > 0', () => { + render() + expect(screen.getByTestId('status-modified-count')).toBeInTheDocument() + expect(screen.getByText('3 pending')).toBeInTheDocument() + }) + + it('does not show modified count when modifiedCount is 0', () => { + render() + expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() + }) + + it('does not show modified count when modifiedCount is not provided', () => { + render() + expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() + }) + it('closes menu after clicking "Open local folder"', () => { render( diff --git a/src/components/TabBar.test.tsx b/src/components/TabBar.test.tsx index e9a568e6..fa150e8a 100644 --- a/src/components/TabBar.test.tsx +++ b/src/components/TabBar.test.tsx @@ -110,6 +110,28 @@ describe('TabBar', () => { expect(onReorderTabs).not.toHaveBeenCalled() }) + it('shows modified indicator dot on modified tabs', () => { + const tabs = makeTabs(['Alpha', 'Beta']) + const isModified = (path: string) => path === tabs[0].entry.path + render() + const indicators = screen.getAllByTestId('tab-modified-indicator') + expect(indicators).toHaveLength(1) + }) + + it('does not show modified indicator when no tabs are modified', () => { + const tabs = makeTabs(['Alpha', 'Beta']) + const isModified = () => false + render() + expect(screen.queryByTestId('tab-modified-indicator')).not.toBeInTheDocument() + }) + + it('shows modified indicator on multiple tabs', () => { + const tabs = makeTabs(['Alpha', 'Beta', 'Gamma']) + const isModified = () => true + render() + expect(screen.getAllByTestId('tab-modified-indicator')).toHaveLength(3) + }) + it('switches tab on click', () => { const onSwitchTab = vi.fn() const tabs = makeTabs(['Alpha', 'Beta']) diff --git a/src/hooks/useEditorSave.test.ts b/src/hooks/useEditorSave.test.ts index 10d75736..c2e0d0a1 100644 --- a/src/hooks/useEditorSave.test.ts +++ b/src/hooks/useEditorSave.test.ts @@ -107,6 +107,43 @@ describe('useEditorSave', () => { }) }) + it('calls onAfterSave callback after successful save', async () => { + const onAfterSave = vi.fn() + const { result } = renderHook(() => + useEditorSave({ updateVaultContent, setTabs, setToastMessage, onAfterSave }) + ) + + act(() => { + result.current.handleContentChange('/test/note.md', 'new content') + }) + + await act(async () => { + await result.current.handleSave() + }) + + expect(onAfterSave).toHaveBeenCalledOnce() + }) + + it('does not call onAfterSave when save fails', async () => { + mockInvokeFn.mockRejectedValueOnce(new Error('Disk full')) + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const onAfterSave = vi.fn() + const { result } = renderHook(() => + useEditorSave({ updateVaultContent, setTabs, setToastMessage, onAfterSave }) + ) + + act(() => { + result.current.handleContentChange('/test/note.md', 'content') + }) + + await act(async () => { + await result.current.handleSave() + }) + + expect(onAfterSave).not.toHaveBeenCalled() + consoleSpy.mockRestore() + }) + it('handleContentChange buffers the latest content', () => { const { result } = renderSaveHook()