191 lines
6.9 KiB
TypeScript
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()
|
|
})
|
|
})
|