89 tests covering: - useNoteActions: slugify, buildNewEntry, entryMatchesTarget, buildNoteContent, resolveNewNote/Type, createNote, createType, navigateWikilink, frontmatter ops - useEntryActions: trash/restore, archive/unarchive, customizeType, reorderSections - useTabManagement: tab selection, close, switch, reorder, replace, localStorage - useVaultLoader: vault loading, modified files, addEntry, updateContent, updateEntry, git history, diff, commitAndPush - useKeyboardNavigation: Cmd+Shift+Arrow tab switching, Cmd+Alt+Arrow note nav - mockFrontmatterHelpers: updateMockFrontmatter, deleteMockFrontmatterProperty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import type { VaultEntry } from '../types'
|
|
import { useEntryActions } from './useEntryActions'
|
|
|
|
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
|
|
path: '/vault/note/test.md',
|
|
filename: 'test.md',
|
|
title: 'Test Note',
|
|
isA: 'Note',
|
|
aliases: [],
|
|
belongsTo: [],
|
|
relatedTo: [],
|
|
status: 'Active',
|
|
owner: null,
|
|
cadence: null,
|
|
archived: false,
|
|
trashed: false,
|
|
trashedAt: null,
|
|
modifiedAt: 1700000000,
|
|
createdAt: 1700000000,
|
|
fileSize: 100,
|
|
snippet: '',
|
|
relationships: {},
|
|
icon: null,
|
|
color: null,
|
|
order: null,
|
|
...overrides,
|
|
})
|
|
|
|
describe('useEntryActions', () => {
|
|
const updateEntry = vi.fn()
|
|
const handleUpdateFrontmatter = vi.fn().mockResolvedValue(undefined)
|
|
const handleDeleteProperty = vi.fn().mockResolvedValue(undefined)
|
|
const setToastMessage = vi.fn()
|
|
|
|
function setup(entries: VaultEntry[] = []) {
|
|
return renderHook(() =>
|
|
useEntryActions({
|
|
entries,
|
|
updateEntry,
|
|
handleUpdateFrontmatter,
|
|
handleDeleteProperty,
|
|
setToastMessage,
|
|
})
|
|
)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('handleTrashNote', () => {
|
|
it('sets trashed frontmatter and updates entry state', async () => {
|
|
const { result } = setup()
|
|
|
|
await act(async () => {
|
|
await result.current.handleTrashNote('/vault/note/test.md')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', true)
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/))
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', {
|
|
trashed: true,
|
|
trashedAt: expect.any(Number),
|
|
})
|
|
expect(setToastMessage).toHaveBeenCalledWith('Note moved to trash')
|
|
})
|
|
})
|
|
|
|
describe('handleRestoreNote', () => {
|
|
it('clears trashed frontmatter and updates entry state', async () => {
|
|
const { result } = setup()
|
|
|
|
await act(async () => {
|
|
await result.current.handleRestoreNote('/vault/note/test.md')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', false)
|
|
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at')
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', {
|
|
trashed: false,
|
|
trashedAt: null,
|
|
})
|
|
expect(setToastMessage).toHaveBeenCalledWith('Note restored from trash')
|
|
})
|
|
})
|
|
|
|
describe('handleArchiveNote', () => {
|
|
it('sets archived frontmatter and updates entry state', async () => {
|
|
const { result } = setup()
|
|
|
|
await act(async () => {
|
|
await result.current.handleArchiveNote('/vault/note/test.md')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'archived', true)
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { archived: true })
|
|
expect(setToastMessage).toHaveBeenCalledWith('Note archived')
|
|
})
|
|
})
|
|
|
|
describe('handleUnarchiveNote', () => {
|
|
it('clears archived frontmatter and updates entry state', async () => {
|
|
const { result } = setup()
|
|
|
|
await act(async () => {
|
|
await result.current.handleUnarchiveNote('/vault/note/test.md')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'archived', false)
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { archived: false })
|
|
expect(setToastMessage).toHaveBeenCalledWith('Note unarchived')
|
|
})
|
|
})
|
|
|
|
describe('handleCustomizeType', () => {
|
|
it('updates icon and color on the type entry', () => {
|
|
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md' })
|
|
const { result } = setup([typeEntry])
|
|
|
|
act(() => {
|
|
result.current.handleCustomizeType('Recipe', 'cooking-pot', 'green')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'icon', 'cooking-pot')
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'color', 'green')
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { icon: 'cooking-pot', color: 'green' })
|
|
})
|
|
|
|
it('does nothing when type entry not found', () => {
|
|
const { result } = setup([])
|
|
|
|
act(() => {
|
|
result.current.handleCustomizeType('NonExistent', 'star', 'red')
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).not.toHaveBeenCalled()
|
|
expect(updateEntry).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('handleReorderSections', () => {
|
|
it('updates order on multiple type entries', () => {
|
|
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/type/note.md' })
|
|
const typeB = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/type/project.md' })
|
|
const { result } = setup([typeA, typeB])
|
|
|
|
act(() => {
|
|
result.current.handleReorderSections([
|
|
{ typeName: 'Note', order: 0 },
|
|
{ typeName: 'Project', order: 1 },
|
|
])
|
|
})
|
|
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/note.md', 'order', 0)
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/project.md', 'order', 1)
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/type/note.md', { order: 0 })
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/type/project.md', { order: 1 })
|
|
})
|
|
|
|
it('skips types that are not found', () => {
|
|
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/type/note.md' })
|
|
const { result } = setup([typeA])
|
|
|
|
act(() => {
|
|
result.current.handleReorderSections([
|
|
{ typeName: 'Note', order: 0 },
|
|
{ typeName: 'Missing', order: 1 },
|
|
])
|
|
})
|
|
|
|
// Only Note's order was set; Missing was skipped
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledTimes(1)
|
|
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/note.md', 'order', 0)
|
|
expect(updateEntry).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
})
|