Files
tolaria/src/hooks/useDeleteActions.test.ts
Test c4136d69b4 refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].

Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.

Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00

223 lines
7.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useDeleteActions } from './useDeleteActions'
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
mockInvoke: vi.fn(),
}))
const { mockInvoke } = await import('../mock-tauri')
const mockInvokeFn = mockInvoke as ReturnType<typeof vi.fn>
function makeEntry(path: string, trashed = false) {
return { path, trashed } as { path: string; trashed: boolean }
}
describe('useDeleteActions', () => {
let onDeselectNote: ReturnType<typeof vi.fn>
let removeEntry: ReturnType<typeof vi.fn>
let setToastMessage: ReturnType<typeof vi.fn>
beforeEach(() => {
onDeselectNote = vi.fn()
removeEntry = vi.fn()
setToastMessage = vi.fn()
mockInvokeFn.mockReset()
})
function renderDeleteActions(entries = [makeEntry('/vault/a.md'), makeEntry('/vault/t.md', true)]) {
return renderHook(() =>
useDeleteActions({
vaultPath: '/vault',
entries,
onDeselectNote,
removeEntry,
setToastMessage,
}),
)
}
// --- trashedCount ---
describe('trashedCount', () => {
it('counts trashed entries', () => {
const { result } = renderDeleteActions([
makeEntry('/vault/a.md'),
makeEntry('/vault/b.md', true),
makeEntry('/vault/c.md', true),
])
expect(result.current.trashedCount).toBe(2)
})
it('returns 0 when no trashed entries', () => {
const { result } = renderDeleteActions([makeEntry('/vault/a.md')])
expect(result.current.trashedCount).toBe(0)
})
})
// --- deleteNoteFromDisk ---
describe('deleteNoteFromDisk', () => {
it('invokes delete_note, closes tab, removes entry, returns true', async () => {
mockInvokeFn.mockResolvedValue(undefined)
const { result } = renderDeleteActions()
let ok: boolean | undefined
await act(async () => {
ok = await result.current.deleteNoteFromDisk('/vault/a.md')
})
expect(ok).toBe(true)
expect(mockInvokeFn).toHaveBeenCalledWith('delete_note', { path: '/vault/a.md' })
expect(onDeselectNote).toHaveBeenCalledWith('/vault/a.md')
expect(removeEntry).toHaveBeenCalledWith('/vault/a.md')
})
it('shows toast and returns false on failure', async () => {
mockInvokeFn.mockRejectedValue(new Error('disk full'))
const { result } = renderDeleteActions()
let ok: boolean | undefined
await act(async () => {
ok = await result.current.deleteNoteFromDisk('/vault/a.md')
})
expect(ok).toBe(false)
expect(setToastMessage).toHaveBeenCalledWith(expect.stringContaining('Failed to delete'))
})
})
// --- handleDeleteNote ---
describe('handleDeleteNote', () => {
it('sets confirmDelete dialog state', async () => {
const { result } = renderDeleteActions()
await act(async () => {
await result.current.handleDeleteNote('/vault/a.md')
})
expect(result.current.confirmDelete).not.toBeNull()
expect(result.current.confirmDelete?.title).toBe('Delete permanently?')
})
it('onConfirm deletes the note and clears dialog', async () => {
mockInvokeFn.mockResolvedValue(undefined)
const { result } = renderDeleteActions()
await act(async () => {
await result.current.handleDeleteNote('/vault/a.md')
})
await act(async () => {
await result.current.confirmDelete!.onConfirm()
})
expect(result.current.confirmDelete).toBeNull()
expect(mockInvokeFn).toHaveBeenCalledWith('delete_note', { path: '/vault/a.md' })
expect(setToastMessage).toHaveBeenCalledWith('Note permanently deleted')
})
})
// --- handleBulkDeletePermanently ---
describe('handleBulkDeletePermanently', () => {
it('sets confirmDelete with correct plural title', () => {
const { result } = renderDeleteActions()
act(() => {
result.current.handleBulkDeletePermanently(['/vault/a.md', '/vault/b.md'])
})
expect(result.current.confirmDelete?.title).toBe('Delete 2 notes permanently?')
})
it('sets confirmDelete with singular title for one note', () => {
const { result } = renderDeleteActions()
act(() => {
result.current.handleBulkDeletePermanently(['/vault/a.md'])
})
expect(result.current.confirmDelete?.title).toBe('Delete 1 note permanently?')
})
it('onConfirm deletes all paths and shows toast', async () => {
mockInvokeFn.mockResolvedValue(undefined)
const { result } = renderDeleteActions()
act(() => {
result.current.handleBulkDeletePermanently(['/vault/a.md', '/vault/b.md'])
})
await act(async () => {
await result.current.confirmDelete!.onConfirm()
})
expect(result.current.confirmDelete).toBeNull()
expect(mockInvokeFn).toHaveBeenCalledTimes(2)
expect(setToastMessage).toHaveBeenCalledWith('2 notes permanently deleted')
})
})
// --- handleEmptyTrash ---
describe('handleEmptyTrash', () => {
it('does nothing when trashedCount is 0', () => {
const { result } = renderDeleteActions([makeEntry('/vault/a.md')])
act(() => {
result.current.handleEmptyTrash()
})
expect(result.current.confirmDelete).toBeNull()
})
it('sets confirmDelete with trash count in message', () => {
const { result } = renderDeleteActions([
makeEntry('/vault/a.md'),
makeEntry('/vault/t1.md', true),
makeEntry('/vault/t2.md', true),
])
act(() => {
result.current.handleEmptyTrash()
})
expect(result.current.confirmDelete?.title).toBe('Empty Trash?')
expect(result.current.confirmDelete?.confirmLabel).toBe('Empty Trash')
})
it('onConfirm invokes empty_trash, closes tabs, removes entries', async () => {
mockInvokeFn.mockResolvedValue(['/vault/t1.md', '/vault/t2.md'])
const { result } = renderDeleteActions([
makeEntry('/vault/a.md'),
makeEntry('/vault/t1.md', true),
makeEntry('/vault/t2.md', true),
])
act(() => {
result.current.handleEmptyTrash()
})
await act(async () => {
await result.current.confirmDelete!.onConfirm()
})
expect(result.current.confirmDelete).toBeNull()
expect(mockInvokeFn).toHaveBeenCalledWith('empty_trash', { vaultPath: '/vault' })
expect(onDeselectNote).toHaveBeenCalledWith('/vault/t1.md')
expect(onDeselectNote).toHaveBeenCalledWith('/vault/t2.md')
expect(removeEntry).toHaveBeenCalledWith('/vault/t1.md')
expect(removeEntry).toHaveBeenCalledWith('/vault/t2.md')
expect(setToastMessage).toHaveBeenCalledWith('2 notes permanently deleted')
})
it('shows error toast when empty_trash fails', async () => {
mockInvokeFn.mockRejectedValue(new Error('oops'))
const { result } = renderDeleteActions([makeEntry('/vault/t.md', true)])
act(() => {
result.current.handleEmptyTrash()
})
await act(async () => {
await result.current.confirmDelete!.onConfirm()
})
expect(setToastMessage).toHaveBeenCalledWith(expect.stringContaining('Failed to empty trash'))
})
})
// --- setConfirmDelete ---
describe('setConfirmDelete', () => {
it('can clear confirmDelete via setConfirmDelete(null)', async () => {
const { result } = renderDeleteActions()
await act(async () => {
await result.current.handleDeleteNote('/vault/a.md')
})
expect(result.current.confirmDelete).not.toBeNull()
act(() => {
result.current.setConfirmDelete(null)
})
expect(result.current.confirmDelete).toBeNull()
})
})
})