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()