Files
tolaria/src/hooks/useVaultBridge.test.ts
2026-06-05 17:20:43 +02:00

191 lines
6.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useVaultBridge } from './useVaultBridge'
import type { VaultEntry } from '../types'
function makeEntry(path: string, title = 'Test'): VaultEntry {
return { path, title, filename: path.split('/').pop()!, content: '', outgoingLinks: [], snippet: '', wordCount: 0, isA: 'Note', status: null, createdAt: null, modifiedAt: null, icon: null, tags: [] } as unknown as VaultEntry
}
function expectVaultDerivedStateReloaded(options: {
reloadVault: ReturnType<typeof vi.fn>
reloadFolders: ReturnType<typeof vi.fn>
reloadViews: ReturnType<typeof vi.fn>
}) {
const { reloadVault, reloadFolders, reloadViews } = options
expect(reloadVault).toHaveBeenCalledOnce()
expect(reloadFolders).toHaveBeenCalledOnce()
expect(reloadViews).toHaveBeenCalledOnce()
}
describe('useVaultBridge', () => {
const onSelectNote = vi.fn()
let reloadVault: ReturnType<typeof vi.fn>
let reloadFolders: ReturnType<typeof vi.fn>
let reloadViews: ReturnType<typeof vi.fn>
let closeAllTabs: ReturnType<typeof vi.fn>
let replaceActiveTab: ReturnType<typeof vi.fn>
let hasUnsavedChanges: ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
reloadVault = vi.fn().mockResolvedValue([])
reloadFolders = vi.fn()
reloadViews = vi.fn()
closeAllTabs = vi.fn()
replaceActiveTab = vi.fn().mockResolvedValue(undefined)
hasUnsavedChanges = vi.fn(() => false)
})
function renderBridge(
entries: VaultEntry[] = [],
activeTabPath: string | null = null,
overrides: Partial<{
hasUnsavedChanges: typeof hasUnsavedChanges
refocusActiveEditor: (path: string) => void
shouldRefocusActiveEditor: () => boolean
}> = {},
) {
const entriesByPath = new Map(entries.map(e => [e.path, e]))
return renderHook(() =>
useVaultBridge({
entriesByPath,
resolvedPath: '/vault',
reloadVault,
reloadFolders,
reloadViews,
closeAllTabs,
replaceActiveTab,
hasUnsavedChanges: overrides.hasUnsavedChanges ?? hasUnsavedChanges,
shouldRefocusActiveEditor: overrides.shouldRefocusActiveEditor,
refocusActiveEditor: overrides.refocusActiveEditor,
onSelectNote,
activeTabPath,
}),
)
}
it('opens a note by path when entry exists', () => {
const entry = makeEntry('/vault/note.md')
const { result } = renderBridge([entry])
act(() => { result.current.openNoteByPath('/vault/note.md') })
expect(onSelectNote).toHaveBeenCalledWith(entry)
expect(reloadVault).not.toHaveBeenCalled()
})
it('opens a note by relative path', () => {
const entry = makeEntry('/vault/note.md')
const { result } = renderBridge([entry])
act(() => { result.current.openNoteByPath('note.md') })
expect(onSelectNote).toHaveBeenCalledWith(entry)
})
it('reloads vault when entry not found', async () => {
const fresh = makeEntry('/vault/new.md')
reloadVault.mockResolvedValue([fresh])
const { result } = renderBridge([])
await act(async () => { result.current.openNoteByPath('/vault/new.md') })
expect(reloadVault).toHaveBeenCalled()
expect(onSelectNote).toHaveBeenCalledWith(fresh)
})
it('handlePulseOpenNote opens existing entry', () => {
const entry = makeEntry('/vault/pulse.md')
const { result } = renderBridge([entry])
act(() => { result.current.handlePulseOpenNote('pulse.md') })
expect(onSelectNote).toHaveBeenCalledWith(entry)
})
it('handlePulseOpenNote does nothing for missing entry', () => {
const { result } = renderBridge([])
act(() => { result.current.handlePulseOpenNote('missing.md') })
expect(onSelectNote).not.toHaveBeenCalled()
})
it('handleAgentFileCreated reloads and opens created note', async () => {
const fresh = makeEntry('/vault/created.md')
reloadVault.mockResolvedValue([fresh])
const { result } = renderBridge([])
await act(async () => { result.current.handleAgentFileCreated('created.md') })
expect(reloadVault).toHaveBeenCalled()
expect(onSelectNote).toHaveBeenCalledWith(fresh)
})
it('handleAgentFileModified refreshes the active tab with fresh disk content', async () => {
const fresh = makeEntry('/vault/active.md', 'Fresh active')
reloadVault.mockResolvedValue([fresh])
const { result } = renderBridge([], '/vault/active.md')
await act(async () => { result.current.handleAgentFileModified('active.md') })
expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews })
expect(closeAllTabs).toHaveBeenCalledOnce()
expect(replaceActiveTab).toHaveBeenCalledWith(fresh)
})
it('refocuses the editor after agent changes refresh the focused active tab', async () => {
const fresh = makeEntry('/vault/active.md', 'Fresh active')
const shouldRefocusActiveEditor = vi.fn(() => true)
const refocusActiveEditor = vi.fn()
reloadVault.mockResolvedValue([fresh])
const { result } = renderBridge([], '/vault/active.md', {
refocusActiveEditor,
shouldRefocusActiveEditor,
})
await act(async () => { result.current.handleAgentFileModified('active.md') })
expect(shouldRefocusActiveEditor).toHaveBeenCalledOnce()
expect(refocusActiveEditor).toHaveBeenCalledWith('/vault/active.md')
})
it('handleAgentFileModified keeps the active tab mounted for other notes', async () => {
const active = makeEntry('/vault/other.md', 'Other')
reloadVault.mockResolvedValue([active])
const { result } = renderBridge([], '/vault/other.md')
await act(async () => { result.current.handleAgentFileModified('active.md') })
expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews })
expect(closeAllTabs).not.toHaveBeenCalled()
expect(replaceActiveTab).not.toHaveBeenCalled()
})
it('keeps unsaved active note content intact while reloading agent changes', async () => {
const fresh = makeEntry('/vault/active.md', 'Fresh active')
reloadVault.mockResolvedValue([fresh])
const hasUnsaved = vi.fn((path: string) => path === '/vault/active.md')
const { result } = renderBridge([], '/vault/active.md', { hasUnsavedChanges: hasUnsaved })
await act(async () => { result.current.handleAgentFileModified('active.md') })
expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews })
expect(closeAllTabs).not.toHaveBeenCalled()
expect(replaceActiveTab).not.toHaveBeenCalled()
})
it('handleAgentVaultChanged reloads vault-derived state without remounting the active note', async () => {
const fresh = makeEntry('/vault/active.md', 'Fresh active')
reloadVault.mockResolvedValue([fresh])
const { result } = renderBridge([], '/vault/active.md')
await act(async () => { result.current.handleAgentVaultChanged() })
expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews })
expect(closeAllTabs).not.toHaveBeenCalled()
expect(replaceActiveTab).not.toHaveBeenCalled()
})
})