test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
|
import { renderHook, act } from '@testing-library/react'
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
2026-03-08 21:37:27 +01:00
|
|
|
import { isTauri, mockInvoke } from '../mock-tauri'
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
import type { VaultEntry } from '../types'
|
2026-04-12 23:14:03 +02:00
|
|
|
import { RAPID_CREATE_NOTE_SETTLE_MS } from './useNoteCreation'
|
2026-03-16 23:34:45 +01:00
|
|
|
import { useNoteActions } from './useNoteActions'
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
import type { NoteActionsConfig } from './useNoteActions'
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }))
|
|
|
|
|
vi.mock('../mock-tauri', () => ({
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
isTauri: vi.fn(() => false),
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
addMockEntry: vi.fn(),
|
|
|
|
|
updateMockContent: vi.fn(),
|
2026-03-06 23:08:18 +01:00
|
|
|
trackMockChange: vi.fn(),
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
mockInvoke: vi.fn().mockResolvedValue(''),
|
|
|
|
|
}))
|
|
|
|
|
vi.mock('./mockFrontmatterHelpers', () => ({
|
|
|
|
|
updateMockFrontmatter: vi.fn().mockReturnValue('---\nupdated: true\n---\n'),
|
|
|
|
|
deleteMockFrontmatterProperty: vi.fn().mockReturnValue('---\n---\n'),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
|
2026-03-15 23:40:47 +01:00
|
|
|
path: '/Users/luca/Laputa/test.md',
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
filename: 'test.md',
|
|
|
|
|
title: 'Test Note',
|
|
|
|
|
isA: 'Note',
|
|
|
|
|
aliases: [],
|
|
|
|
|
belongsTo: [],
|
|
|
|
|
relatedTo: [],
|
|
|
|
|
status: 'Active',
|
|
|
|
|
archived: false,
|
|
|
|
|
modifiedAt: 1700000000,
|
|
|
|
|
createdAt: 1700000000,
|
|
|
|
|
fileSize: 100,
|
|
|
|
|
snippet: '',
|
2026-02-26 20:50:29 +01:00
|
|
|
wordCount: 0,
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
relationships: {},
|
|
|
|
|
icon: null,
|
|
|
|
|
color: null,
|
|
|
|
|
order: null,
|
2026-02-25 15:04:49 +01:00
|
|
|
outgoingLinks: [],
|
2026-04-08 14:01:19 +02:00
|
|
|
template: null,
|
|
|
|
|
sort: null,
|
|
|
|
|
sidebarLabel: null,
|
|
|
|
|
view: null,
|
|
|
|
|
visible: null,
|
|
|
|
|
properties: {},
|
|
|
|
|
organized: false,
|
|
|
|
|
favorite: false,
|
|
|
|
|
favoriteIndex: null,
|
|
|
|
|
listPropertiesDisplay: [],
|
2026-04-06 13:08:17 +02:00
|
|
|
hasH1: false,
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
...overrides,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('useNoteActions hook', () => {
|
|
|
|
|
const addEntry = vi.fn()
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
const removeEntry = vi.fn()
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const updateEntry = vi.fn()
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
const setToastMessage = vi.fn()
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const makeConfig = (entries: VaultEntry[] = []): NoteActionsConfig => ({
|
2026-03-08 22:15:08 +01:00
|
|
|
addEntry, removeEntry, entries, setToastMessage, updateEntry, vaultPath: '/test/vault',
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
})
|
|
|
|
|
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
vi.mocked(isTauri).mockReturnValue(false)
|
2026-04-11 12:12:12 +02:00
|
|
|
vi.useRealTimers()
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-22 22:37:01 +02:00
|
|
|
function renderActions(entries: VaultEntry[] = []) {
|
|
|
|
|
return renderHook(() => useNoteActions(makeConfig(entries)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
async function flushAsyncWork() {
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createImmediateEntry(type?: string) {
|
2026-04-22 22:37:01 +02:00
|
|
|
vi.spyOn(Date, 'now').mockReturnValue(1700000000000)
|
|
|
|
|
const { result } = renderActions()
|
2026-04-28 15:01:32 +02:00
|
|
|
await act(async () => {
|
2026-04-22 22:37:01 +02:00
|
|
|
result.current.handleCreateNoteImmediate(type)
|
2026-04-28 15:01:32 +02:00
|
|
|
await flushAsyncWork()
|
2026-04-22 22:37:01 +02:00
|
|
|
})
|
|
|
|
|
const [createdEntry] = addEntry.mock.calls[0]
|
|
|
|
|
vi.restoreAllMocks()
|
|
|
|
|
return createdEntry as VaultEntry
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
{
|
|
|
|
|
name: 'handleCreateNote',
|
|
|
|
|
run: (result: ReturnType<typeof renderActions>['result']) => result.current.handleCreateNote('Test Note', 'Note'),
|
|
|
|
|
expectedTitle: 'Test Note',
|
|
|
|
|
expectedType: 'Note',
|
|
|
|
|
expectedPathFragment: 'test-note.md',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'handleCreateType',
|
|
|
|
|
run: (result: ReturnType<typeof renderActions>['result']) => result.current.handleCreateType('Recipe'),
|
|
|
|
|
expectedTitle: 'Recipe',
|
|
|
|
|
expectedType: 'Type',
|
|
|
|
|
expectedPathFragment: 'recipe.md',
|
|
|
|
|
},
|
|
|
|
|
])('$name creates the expected entry', ({ run, expectedTitle, expectedType, expectedPathFragment }) => {
|
|
|
|
|
const { result } = renderActions()
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
act(() => {
|
2026-04-22 22:37:01 +02:00
|
|
|
run(result)
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
2026-03-08 22:15:08 +01:00
|
|
|
const [createdEntry] = addEntry.mock.calls[0]
|
2026-04-22 22:37:01 +02:00
|
|
|
expect(createdEntry.title).toBe(expectedTitle)
|
|
|
|
|
expect(createdEntry.isA).toBe(expectedType)
|
|
|
|
|
expect(createdEntry.path).toContain(expectedPathFragment)
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
})
|
|
|
|
|
|
2026-02-26 11:41:46 +01:00
|
|
|
it('handleCreateNote opens tab immediately (before addEntry resolves)', () => {
|
|
|
|
|
const callOrder: string[] = []
|
|
|
|
|
const trackedAddEntry = vi.fn(() => { callOrder.push('addEntry') })
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
config.addEntry = trackedAddEntry
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNote('Fast Note', 'Note')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Tab should be open with the new note
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].entry.title).toBe('Fast Note')
|
2026-03-15 19:43:00 +01:00
|
|
|
expect(result.current.activeTabPath).toContain('fast-note.md')
|
2026-02-26 11:41:46 +01:00
|
|
|
})
|
|
|
|
|
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
it('handleNavigateWikilink finds entry by title', async () => {
|
2026-03-15 23:40:47 +01:00
|
|
|
const target = makeEntry({ title: 'Target Note', path: '/vault/target.md' })
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig([target])))
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleNavigateWikilink('Target Note')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-15 23:40:47 +01:00
|
|
|
expect(result.current.activeTabPath).toBe('/vault/target.md')
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleNavigateWikilink warns when target not found', () => {
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleNavigateWikilink('Nonexistent')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith('Navigation target not found: Nonexistent')
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
it('handleUpdateFrontmatter calls updateEntry with mapped patch', async () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleUpdateFrontmatter('/vault/note.md', 'status', 'Done')
|
|
|
|
|
})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note.md', { status: 'Done' })
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Property updated')
|
|
|
|
|
})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
it('handleUpdateFrontmatter syncs is_a and color changes to entries', async () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleUpdateFrontmatter('/vault/note.md', 'is_a', 'Project')
|
|
|
|
|
})
|
|
|
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note.md', { isA: 'Project' })
|
|
|
|
|
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleUpdateFrontmatter('/vault/note.md', 'color', 'blue')
|
|
|
|
|
})
|
|
|
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note.md', { color: 'blue' })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleDeleteProperty calls updateEntry with null/default values', async () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleDeleteProperty('/vault/note.md', 'status')
|
|
|
|
|
})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
expect(updateEntry).toHaveBeenCalledWith('/vault/note.md', { status: null })
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Property deleted')
|
|
|
|
|
})
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate creates note with timestamp-based title', async () => {
|
|
|
|
|
const createdEntry = await createImmediateEntry()
|
2026-04-06 13:08:17 +02:00
|
|
|
expect(createdEntry.title).toBe('Untitled Note 1700000000')
|
|
|
|
|
expect(createdEntry.filename).toBe('untitled-note-1700000000.md')
|
2026-02-24 23:03:01 +01:00
|
|
|
expect(createdEntry.isA).toBe('Note')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate generates unique names on rapid calls via timestamp', async () => {
|
2026-04-11 12:12:12 +02:00
|
|
|
vi.useFakeTimers()
|
2026-04-06 13:08:17 +02:00
|
|
|
let ts = 1700000000000
|
|
|
|
|
vi.spyOn(Date, 'now').mockImplementation(() => { ts += 1000; return ts })
|
2026-02-24 23:03:01 +01:00
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
await act(async () => {
|
2026-02-24 23:03:01 +01:00
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
2026-04-28 15:01:32 +02:00
|
|
|
await flushAsyncWork()
|
|
|
|
|
})
|
|
|
|
|
await act(async () => {
|
|
|
|
|
vi.advanceTimersByTime(RAPID_CREATE_NOTE_SETTLE_MS * 2)
|
|
|
|
|
await flushAsyncWork()
|
2026-02-24 23:03:01 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(3)
|
2026-04-06 13:08:17 +02:00
|
|
|
const filenames = addEntry.mock.calls.map(([e]: [VaultEntry]) => e.filename)
|
|
|
|
|
// Each call consumes Date.now() multiple times, so just verify uniqueness and pattern
|
|
|
|
|
expect(new Set(filenames).size).toBe(3)
|
|
|
|
|
for (const fn of filenames) {
|
|
|
|
|
expect(fn).toMatch(/^untitled-note-\d+\.md$/)
|
|
|
|
|
}
|
|
|
|
|
vi.restoreAllMocks()
|
2026-02-24 23:03:01 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate accepts custom type', async () => {
|
|
|
|
|
const createdEntry = await createImmediateEntry('Project')
|
2026-04-06 13:08:17 +02:00
|
|
|
expect(createdEntry.filename).toMatch(/^untitled-project-\d+\.md$/)
|
2026-02-24 23:03:01 +01:00
|
|
|
expect(createdEntry.isA).toBe('Project')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-02 08:37:06 +01:00
|
|
|
it('handleCreateNote uses default template for Project type', () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNote('My Project', 'Project')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-08 22:15:08 +01:00
|
|
|
const tabContent = result.current.tabs[0].content
|
|
|
|
|
expect(tabContent).toContain('## Objective')
|
|
|
|
|
expect(tabContent).toContain('## Key Results')
|
2026-03-02 08:37:06 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleCreateNote uses custom template from type entry', () => {
|
|
|
|
|
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', template: '## Ingredients\n\n## Steps\n\n' })
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig([typeEntry])))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNote('Pasta', 'Recipe')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-08 22:15:08 +01:00
|
|
|
const tabContent = result.current.tabs[0].content
|
|
|
|
|
expect(tabContent).toContain('## Ingredients')
|
|
|
|
|
expect(tabContent).toContain('## Steps')
|
2026-03-02 08:37:06 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it.each([
|
|
|
|
|
['Q&A', (entry: VaultEntry) => { expect(entry.isA).toBe('Q&A') }],
|
|
|
|
|
['+++', (entry: VaultEntry) => { expect(entry.filename).not.toBe('.md') }],
|
|
|
|
|
])('handleCreateNoteImmediate handles custom type "%s"', async (typeName, assertEntry) => {
|
2026-03-11 16:42:32 +01:00
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
await act(async () => {
|
|
|
|
|
expect(() => { result.current.handleCreateNoteImmediate(typeName) }).not.toThrow()
|
|
|
|
|
await flushAsyncWork()
|
|
|
|
|
})
|
2026-03-11 16:42:32 +01:00
|
|
|
|
|
|
|
|
const [entry] = addEntry.mock.calls[0]
|
|
|
|
|
expect(entry.path).not.toContain('//')
|
2026-04-28 15:01:32 +02:00
|
|
|
assertEntry(entry)
|
2026-03-11 16:42:32 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate uses template for typed notes', async () => {
|
2026-03-02 08:37:06 +01:00
|
|
|
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', template: '## Custom Template\n\n' })
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig([typeEntry])))
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
await act(async () => {
|
2026-03-02 08:37:06 +01:00
|
|
|
result.current.handleCreateNoteImmediate('Project')
|
2026-04-28 15:01:32 +02:00
|
|
|
await flushAsyncWork()
|
2026-03-02 08:37:06 +01:00
|
|
|
})
|
|
|
|
|
|
2026-03-08 22:15:08 +01:00
|
|
|
const tabContent = result.current.tabs[0].content
|
|
|
|
|
expect(tabContent).toContain('## Custom Template')
|
2026-03-02 08:37:06 +01:00
|
|
|
})
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
it('handleUpdateFrontmatter does not call updateEntry for unknown keys', async () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleUpdateFrontmatter('/vault/note.md', 'custom_field', 'value')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(updateEntry).not.toHaveBeenCalled()
|
|
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Property updated')
|
|
|
|
|
})
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
|
2026-02-27 14:11:31 +01:00
|
|
|
describe('pending save lifecycle', () => {
|
2026-04-28 15:01:32 +02:00
|
|
|
it.each([
|
|
|
|
|
['start', 'Pending Test', 'pending-test.md', 'addPendingSave'],
|
|
|
|
|
['completion', 'Persist OK', 'persist-ok.md', 'removePendingSave'],
|
|
|
|
|
])('createAndPersist calls pending-save callback on %s (non-Tauri)', async (
|
|
|
|
|
_phase,
|
|
|
|
|
title,
|
|
|
|
|
pathFragment,
|
|
|
|
|
callbackName,
|
|
|
|
|
) => {
|
2026-02-27 14:11:31 +01:00
|
|
|
const addPendingSave = vi.fn()
|
|
|
|
|
const removePendingSave = vi.fn()
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
config.addPendingSave = addPendingSave
|
|
|
|
|
config.removePendingSave = removePendingSave
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-04-28 15:01:32 +02:00
|
|
|
result.current.handleCreateNote(title, 'Note')
|
|
|
|
|
await flushAsyncWork()
|
2026-02-27 14:11:31 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
const callback = callbackName === 'addPendingSave' ? addPendingSave : removePendingSave
|
|
|
|
|
expect(callback).toHaveBeenCalledWith(expect.stringContaining(pathFragment))
|
2026-02-27 14:11:31 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('createAndPersist calls removePendingSave AND reverts when persist fails (Tauri)', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockRejectedValueOnce(new Error('disk full'))
|
|
|
|
|
const addPendingSave = vi.fn()
|
|
|
|
|
const removePendingSave = vi.fn()
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
config.addPendingSave = addPendingSave
|
|
|
|
|
config.removePendingSave = removePendingSave
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNote('Fail Save', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-15 19:43:00 +01:00
|
|
|
expect(addPendingSave).toHaveBeenCalledWith(expect.stringContaining('fail-save.md'))
|
|
|
|
|
expect(removePendingSave).toHaveBeenCalledWith(expect.stringContaining('fail-save.md'))
|
|
|
|
|
expect(removeEntry).toHaveBeenCalledWith(expect.stringContaining('fail-save.md'))
|
2026-02-27 14:11:31 +01:00
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Failed to create note — disk write error')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate creates the backing file before opening the note', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined)
|
|
|
|
|
const addPendingSave = vi.fn()
|
|
|
|
|
const removePendingSave = vi.fn()
|
|
|
|
|
const onNewNotePersisted = vi.fn()
|
2026-02-27 14:11:31 +01:00
|
|
|
const config = makeConfig()
|
2026-04-28 15:01:32 +02:00
|
|
|
config.addPendingSave = addPendingSave
|
|
|
|
|
config.removePendingSave = removePendingSave
|
|
|
|
|
config.onNewNotePersisted = onNewNotePersisted
|
2026-02-27 14:11:31 +01:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
2026-04-28 15:01:32 +02:00
|
|
|
await flushAsyncWork()
|
2026-02-27 14:11:31 +01:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
const createdPath = expect.stringMatching(/untitled-note-\d+\.md$/)
|
|
|
|
|
expect(vi.mocked(invoke)).toHaveBeenCalledWith('create_note_content', {
|
|
|
|
|
path: createdPath,
|
|
|
|
|
content: expect.stringContaining('type: Note'),
|
|
|
|
|
})
|
|
|
|
|
expect(addPendingSave).toHaveBeenCalledWith(createdPath)
|
|
|
|
|
expect(removePendingSave).toHaveBeenCalledWith(createdPath)
|
|
|
|
|
expect(onNewNotePersisted).toHaveBeenCalledOnce()
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(result.current.tabs[0].entry.path).toMatch(/untitled-note-\d+\.md$/)
|
2026-02-27 14:11:31 +01:00
|
|
|
})
|
2026-03-02 03:07:02 +01:00
|
|
|
|
|
|
|
|
it('calls onNewNotePersisted after successful disk write (non-Tauri)', async () => {
|
|
|
|
|
const onNewNotePersisted = vi.fn()
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
config.onNewNotePersisted = onNewNotePersisted
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNote('Persist Callback', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(onNewNotePersisted).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not call onNewNotePersisted when disk write fails (Tauri)', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockRejectedValueOnce(new Error('disk full'))
|
|
|
|
|
const onNewNotePersisted = vi.fn()
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
config.onNewNotePersisted = onNewNotePersisted
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNote('Fail Persist', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(onNewNotePersisted).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-02-27 14:11:31 +01:00
|
|
|
})
|
|
|
|
|
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
describe('optimistic error recovery (Tauri mode)', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
2026-03-15 19:43:00 +01:00
|
|
|
['handleCreateNote', 'Failing Note', 'Note', 'failing-note.md'],
|
2026-03-16 16:17:58 +01:00
|
|
|
['handleCreateType', 'Recipe', 'Type', 'recipe.md'],
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
])('reverts optimistic creation via %s when disk write fails', async (method, title, type, pathFragment) => {
|
|
|
|
|
vi.mocked(invoke).mockRejectedValueOnce(new Error('disk full'))
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
if (method === 'handleCreateNote') result.current.handleCreateNote(title, type)
|
|
|
|
|
else result.current.handleCreateType(title)
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(removeEntry).toHaveBeenCalledWith(expect.stringContaining(pathFragment))
|
2026-04-22 22:37:01 +02:00
|
|
|
expect(setToastMessage).toHaveBeenCalledWith(
|
|
|
|
|
type === 'Type'
|
|
|
|
|
? 'Failed to create type — disk write error'
|
|
|
|
|
: 'Failed to create note — disk write error',
|
|
|
|
|
)
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not revert when disk write succeeds', async () => {
|
|
|
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined)
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNote('Good Note', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(removeEntry).not.toHaveBeenCalled()
|
|
|
|
|
expect(setToastMessage).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
it('handleCreateNoteImmediate writes each rapid note before opening it', async () => {
|
2026-04-11 12:12:12 +02:00
|
|
|
vi.useFakeTimers()
|
2026-04-28 15:01:32 +02:00
|
|
|
vi.mocked(invoke).mockResolvedValue(undefined)
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
2026-04-28 15:01:32 +02:00
|
|
|
await act(async () => {
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
2026-04-28 15:01:32 +02:00
|
|
|
await flushAsyncWork()
|
|
|
|
|
})
|
|
|
|
|
await act(async () => {
|
|
|
|
|
vi.advanceTimersByTime(RAPID_CREATE_NOTE_SETTLE_MS * 2)
|
|
|
|
|
await flushAsyncWork()
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(3)
|
2026-04-28 15:01:32 +02:00
|
|
|
expect(vi.mocked(invoke).mock.calls.filter(([command]) => command === 'create_note_content')).toHaveLength(3)
|
2026-02-27 18:17:47 +01:00
|
|
|
expect(removeEntry).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
|
|
|
})
|
2026-03-08 21:37:27 +01:00
|
|
|
|
2026-03-15 23:40:47 +01:00
|
|
|
describe('type change does not move file', () => {
|
|
|
|
|
it('changing type only updates frontmatter, does not move file', async () => {
|
|
|
|
|
const entry = makeEntry({ path: '/test/vault/my-note.md', filename: 'my-note.md', title: 'My Note', isA: 'Note' })
|
|
|
|
|
const config = makeConfig([entry])
|
2026-03-08 21:37:27 +01:00
|
|
|
vi.mocked(mockInvoke).mockResolvedValue('')
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-03-15 23:40:47 +01:00
|
|
|
await result.current.handleUpdateFrontmatter('/test/vault/my-note.md', 'type', 'Quarter')
|
2026-03-08 21:37:27 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Property updated')
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-11 18:47:58 +01:00
|
|
|
|
2026-04-07 20:43:13 +02:00
|
|
|
describe('note open is read-only', () => {
|
2026-04-29 11:42:37 +02:00
|
|
|
it('does not sync title or reload entry when opening or freshness-validating a note', async () => {
|
2026-03-18 02:37:29 +01:00
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
const entry = makeEntry({ path: '/test/vault/qa-test.md', filename: 'qa-test.md', title: 'Qa Test' })
|
2026-04-29 11:42:37 +02:00
|
|
|
vi.mocked(invoke).mockImplementation(async (command) => {
|
|
|
|
|
if (command === 'validate_note_content') return true
|
|
|
|
|
if (command === 'get_note_content') return '# Qa Test\n'
|
|
|
|
|
return null
|
|
|
|
|
})
|
2026-03-18 02:37:29 +01:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig([entry])))
|
|
|
|
|
|
|
|
|
|
await act(async () => { await result.current.handleSelectNote(entry) })
|
2026-04-07 20:43:13 +02:00
|
|
|
const callCountAfterFirstOpen = vi.mocked(invoke).mock.calls.length
|
2026-03-18 02:37:29 +01:00
|
|
|
|
|
|
|
|
const desyncedEntry = { ...entry, title: 'Wrong Title Desynced' }
|
|
|
|
|
await act(async () => { await result.current.handleSelectNote(desyncedEntry) })
|
|
|
|
|
|
2026-04-29 11:42:37 +02:00
|
|
|
expect(vi.mocked(invoke)).toHaveBeenCalledTimes(callCountAfterFirstOpen + 1)
|
2026-04-07 20:43:13 +02:00
|
|
|
expect(vi.mocked(invoke).mock.calls).toEqual([
|
|
|
|
|
['get_note_content', { path: '/test/vault/qa-test.md' }],
|
2026-04-29 11:42:37 +02:00
|
|
|
['validate_note_content', { path: '/test/vault/qa-test.md', content: '# Qa Test\n' }],
|
2026-04-07 20:43:13 +02:00
|
|
|
])
|
|
|
|
|
expect(result.current.tabs[0].entry.title).toBe('Qa Test')
|
2026-03-18 02:37:29 +01:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-11 18:47:58 +01:00
|
|
|
describe('rename note updates wikilinks', () => {
|
|
|
|
|
it('handleRenameNote passes entry title as old_title to rename_note', async () => {
|
|
|
|
|
const entry = makeEntry({
|
2026-03-15 23:40:47 +01:00
|
|
|
path: '/test/vault/weekly-review.md',
|
2026-03-11 18:47:58 +01:00
|
|
|
filename: 'weekly-review.md',
|
|
|
|
|
title: 'Weekly Review',
|
|
|
|
|
})
|
|
|
|
|
const replaceEntry = vi.fn()
|
|
|
|
|
const config = makeConfig([entry])
|
|
|
|
|
config.replaceEntry = replaceEntry
|
|
|
|
|
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation(async (cmd: string) => {
|
2026-03-15 23:40:47 +01:00
|
|
|
if (cmd === 'rename_note') return { new_path: '/test/vault/sprint-retro.md', updated_files: 2 }
|
2026-03-11 18:47:58 +01:00
|
|
|
if (cmd === 'get_note_content') return '---\nIs A: Note\n---\n# Sprint Retro\n'
|
|
|
|
|
return ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleRenameNote(
|
2026-03-15 23:40:47 +01:00
|
|
|
'/test/vault/weekly-review.md',
|
2026-03-11 18:47:58 +01:00
|
|
|
'Sprint Retro',
|
|
|
|
|
'/test/vault',
|
|
|
|
|
replaceEntry,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith('rename_note', expect.objectContaining({
|
|
|
|
|
vault_path: '/test/vault',
|
2026-03-15 23:40:47 +01:00
|
|
|
old_path: '/test/vault/weekly-review.md',
|
2026-03-11 18:47:58 +01:00
|
|
|
new_title: 'Sprint Retro',
|
|
|
|
|
old_title: 'Weekly Review',
|
|
|
|
|
}))
|
2026-04-10 19:57:21 +02:00
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Updated 2 notes')
|
2026-03-11 18:47:58 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleRenameNote passes null old_title when entry not found', async () => {
|
|
|
|
|
const config = makeConfig([])
|
|
|
|
|
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation(async (cmd: string) => {
|
2026-03-15 23:40:47 +01:00
|
|
|
if (cmd === 'rename_note') return { new_path: '/test/vault/new.md', updated_files: 0 }
|
2026-03-11 18:47:58 +01:00
|
|
|
if (cmd === 'get_note_content') return '# New\n'
|
|
|
|
|
return ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleRenameNote(
|
2026-03-15 23:40:47 +01:00
|
|
|
'/test/vault/old.md', 'New', '/test/vault', vi.fn(),
|
2026-03-11 18:47:58 +01:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith('rename_note', expect.objectContaining({
|
|
|
|
|
old_title: null,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleUpdateFrontmatter triggers rename when title key is changed', async () => {
|
|
|
|
|
const entry = makeEntry({
|
2026-03-15 23:40:47 +01:00
|
|
|
path: '/test/vault/old-name.md',
|
2026-03-11 18:47:58 +01:00
|
|
|
filename: 'old-name.md',
|
|
|
|
|
title: 'Old Name',
|
|
|
|
|
})
|
2026-04-14 15:40:46 +02:00
|
|
|
const onPathRenamed = vi.fn()
|
2026-03-11 18:47:58 +01:00
|
|
|
const replaceEntry = vi.fn()
|
|
|
|
|
const config = makeConfig([entry])
|
2026-04-14 15:40:46 +02:00
|
|
|
config.onPathRenamed = onPathRenamed
|
2026-03-11 18:47:58 +01:00
|
|
|
config.replaceEntry = replaceEntry
|
|
|
|
|
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation(async (cmd: string) => {
|
2026-03-15 23:40:47 +01:00
|
|
|
if (cmd === 'rename_note') return { new_path: '/test/vault/new-name.md', updated_files: 1 }
|
2026-03-11 18:47:58 +01:00
|
|
|
if (cmd === 'get_note_content') return '---\ntitle: New Name\n---\n# New Name\n'
|
|
|
|
|
return ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
// Open a tab for the entry so the rename can find it via tabsRef
|
|
|
|
|
await act(async () => { result.current.handleSelectNote(entry) })
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-03-15 23:40:47 +01:00
|
|
|
await result.current.handleUpdateFrontmatter('/test/vault/old-name.md', 'title', 'New Name')
|
2026-03-11 18:47:58 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith('rename_note', expect.objectContaining({
|
2026-03-15 23:40:47 +01:00
|
|
|
old_path: '/test/vault/old-name.md',
|
2026-03-11 18:47:58 +01:00
|
|
|
new_title: 'New Name',
|
|
|
|
|
old_title: 'Old Name',
|
|
|
|
|
}))
|
|
|
|
|
expect(replaceEntry).toHaveBeenCalledWith(
|
2026-03-15 23:40:47 +01:00
|
|
|
'/test/vault/old-name.md',
|
|
|
|
|
expect.objectContaining({ path: '/test/vault/new-name.md', title: 'New Name' }),
|
2026-03-11 18:47:58 +01:00
|
|
|
)
|
2026-04-14 15:40:46 +02:00
|
|
|
expect(onPathRenamed).toHaveBeenCalledWith('/test/vault/old-name.md', '/test/vault/new-name.md')
|
2026-03-11 18:47:58 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleUpdateFrontmatter does not trigger rename for non-title keys', async () => {
|
|
|
|
|
const config = makeConfig()
|
|
|
|
|
vi.mocked(mockInvoke).mockResolvedValue('')
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(config))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleUpdateFrontmatter('/vault/note.md', 'status', 'Done')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(mockInvoke).not.toHaveBeenCalledWith('rename_note', expect.anything())
|
|
|
|
|
})
|
|
|
|
|
})
|
test: add unit tests for hooks — useNoteActions, useEntryActions, useTabManagement, useVaultLoader, useKeyboardNavigation, mockFrontmatterHelpers
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>
2026-02-22 12:02:19 +01:00
|
|
|
})
|