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'
|
|
|
|
|
import { isTauri } 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'
|
|
|
|
|
import {
|
|
|
|
|
slugify,
|
|
|
|
|
buildNewEntry,
|
|
|
|
|
generateUntitledName,
|
|
|
|
|
entryMatchesTarget,
|
|
|
|
|
buildNoteContent,
|
|
|
|
|
resolveNewNote,
|
|
|
|
|
resolveNewType,
|
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
|
|
|
frontmatterToEntryPatch,
|
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
|
|
|
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
|
|
|
|
|
|
|
|
// Mock dependencies
|
|
|
|
|
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(),
|
|
|
|
|
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 => ({
|
|
|
|
|
path: '/Users/luca/Laputa/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: '',
|
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: [],
|
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('slugify', () => {
|
|
|
|
|
it('converts text to lowercase kebab-case', () => {
|
|
|
|
|
expect(slugify('Hello World')).toBe('hello-world')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('removes special characters', () => {
|
|
|
|
|
expect(slugify('My Project! @#$%')).toBe('my-project')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('strips leading and trailing hyphens', () => {
|
|
|
|
|
expect(slugify('--hello--')).toBe('hello')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handles empty string', () => {
|
|
|
|
|
expect(slugify('')).toBe('')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('collapses multiple separators into one hyphen', () => {
|
|
|
|
|
expect(slugify('hello world---foo')).toBe('hello-world-foo')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('buildNewEntry', () => {
|
|
|
|
|
it('creates a VaultEntry with correct fields', () => {
|
|
|
|
|
const entry = buildNewEntry({
|
|
|
|
|
path: '/vault/note/my-note.md',
|
|
|
|
|
slug: 'my-note',
|
|
|
|
|
title: 'My Note',
|
|
|
|
|
type: 'Note',
|
|
|
|
|
status: 'Active',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(entry.path).toBe('/vault/note/my-note.md')
|
|
|
|
|
expect(entry.filename).toBe('my-note.md')
|
|
|
|
|
expect(entry.title).toBe('My Note')
|
|
|
|
|
expect(entry.isA).toBe('Note')
|
|
|
|
|
expect(entry.status).toBe('Active')
|
|
|
|
|
expect(entry.archived).toBe(false)
|
|
|
|
|
expect(entry.trashed).toBe(false)
|
|
|
|
|
expect(entry.modifiedAt).toBeGreaterThan(0)
|
|
|
|
|
expect(entry.createdAt).toBe(entry.modifiedAt)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('sets null status when provided', () => {
|
|
|
|
|
const entry = buildNewEntry({
|
|
|
|
|
path: '/vault/topic/ai.md',
|
|
|
|
|
slug: 'ai',
|
|
|
|
|
title: 'AI',
|
|
|
|
|
type: 'Topic',
|
|
|
|
|
status: null,
|
|
|
|
|
})
|
|
|
|
|
expect(entry.status).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('generateUntitledName', () => {
|
|
|
|
|
it('returns base name when no conflicts', () => {
|
|
|
|
|
expect(generateUntitledName([], 'Note')).toBe('Untitled note')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('appends counter when base name exists', () => {
|
|
|
|
|
const entries = [makeEntry({ title: 'Untitled note' })]
|
|
|
|
|
expect(generateUntitledName(entries, 'Note')).toBe('Untitled note 2')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('increments counter past existing numbered entries', () => {
|
|
|
|
|
const entries = [
|
|
|
|
|
makeEntry({ title: 'Untitled note' }),
|
|
|
|
|
makeEntry({ title: 'Untitled note 2' }),
|
|
|
|
|
makeEntry({ title: 'Untitled note 3' }),
|
|
|
|
|
]
|
|
|
|
|
expect(generateUntitledName(entries, 'Note')).toBe('Untitled note 4')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses type name in lowercase', () => {
|
|
|
|
|
expect(generateUntitledName([], 'Project')).toBe('Untitled project')
|
|
|
|
|
})
|
2026-02-24 23:03:01 +01:00
|
|
|
|
|
|
|
|
it('avoids names in the pending set', () => {
|
|
|
|
|
const pending = new Set(['Untitled note'])
|
|
|
|
|
expect(generateUntitledName([], 'Note', pending)).toBe('Untitled note 2')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('avoids both existing and pending names', () => {
|
|
|
|
|
const entries = [makeEntry({ title: 'Untitled note' })]
|
|
|
|
|
const pending = new Set(['Untitled note 2'])
|
|
|
|
|
expect(generateUntitledName(entries, 'Note', pending)).toBe('Untitled note 3')
|
|
|
|
|
})
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('entryMatchesTarget', () => {
|
|
|
|
|
it('matches by exact title (case-insensitive)', () => {
|
|
|
|
|
const entry = makeEntry({ title: 'My Project' })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'my project', 'my project')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('matches by alias', () => {
|
|
|
|
|
const entry = makeEntry({ aliases: ['MP', 'TheProject'] })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'mp', 'mp')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('matches by path stem (relative to Laputa)', () => {
|
|
|
|
|
const entry = makeEntry({ path: '/Users/luca/Laputa/project/my-project.md' })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'project/my-project', 'project/my-project')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('matches by filename stem', () => {
|
|
|
|
|
const entry = makeEntry({ filename: 'my-project.md' })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'my-project', 'my-project')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('matches when target as words matches title', () => {
|
|
|
|
|
const entry = makeEntry({ title: 'my project' })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'project/my-project', 'my project')).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('returns false when nothing matches', () => {
|
|
|
|
|
const entry = makeEntry({ title: 'Something Else', aliases: [], filename: 'else.md' })
|
|
|
|
|
expect(entryMatchesTarget(entry, 'nonexistent', 'nonexistent')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('buildNoteContent', () => {
|
|
|
|
|
it('generates frontmatter with status for regular types', () => {
|
|
|
|
|
const content = buildNoteContent('My Note', 'Note', 'Active')
|
2026-02-23 15:24:57 +01:00
|
|
|
expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n\n# My Note\n\n')
|
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('omits status when null', () => {
|
|
|
|
|
const content = buildNoteContent('AI', 'Topic', null)
|
2026-02-23 15:24:57 +01:00
|
|
|
expect(content).toBe('---\ntitle: AI\ntype: Topic\n---\n\n# AI\n\n')
|
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
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('resolveNewNote', () => {
|
|
|
|
|
it('uses TYPE_FOLDER_MAP for known types', () => {
|
|
|
|
|
const { entry, content } = resolveNewNote('My Project', 'Project')
|
|
|
|
|
expect(entry.path).toBe('/Users/luca/Laputa/project/my-project.md')
|
|
|
|
|
expect(entry.isA).toBe('Project')
|
|
|
|
|
expect(entry.status).toBe('Active')
|
2026-02-23 15:24:57 +01:00
|
|
|
expect(content).toContain('type: Project')
|
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(content).toContain('status: Active')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('falls back to slugified type for custom types', () => {
|
|
|
|
|
const { entry } = resolveNewNote('First Recipe', 'Recipe')
|
|
|
|
|
expect(entry.path).toBe('/Users/luca/Laputa/recipe/first-recipe.md')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('omits status for Topic type', () => {
|
|
|
|
|
const { entry, content } = resolveNewNote('Machine Learning', 'Topic')
|
|
|
|
|
expect(entry.status).toBeNull()
|
|
|
|
|
expect(content).not.toContain('status:')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('omits status for Person type', () => {
|
|
|
|
|
const { entry } = resolveNewNote('John Doe', 'Person')
|
|
|
|
|
expect(entry.status).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('resolveNewType', () => {
|
|
|
|
|
it('creates a type entry in the type folder', () => {
|
|
|
|
|
const { entry, content } = resolveNewType('Recipe')
|
|
|
|
|
expect(entry.path).toBe('/Users/luca/Laputa/type/recipe.md')
|
|
|
|
|
expect(entry.isA).toBe('Type')
|
|
|
|
|
expect(entry.status).toBeNull()
|
2026-02-23 15:24:57 +01:00
|
|
|
expect(content).toContain('type: Type')
|
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(content).toContain('# Recipe')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
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
|
|
|
describe('frontmatterToEntryPatch', () => {
|
|
|
|
|
it.each([
|
2026-02-23 15:24:57 +01:00
|
|
|
['type', 'Project', { isA: 'Project' }],
|
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
|
|
|
['is_a', 'Project', { isA: 'Project' }],
|
|
|
|
|
['status', 'Done', { status: 'Done' }],
|
|
|
|
|
['color', 'red', { color: 'red' }],
|
|
|
|
|
['icon', 'star', { icon: 'star' }],
|
|
|
|
|
['owner', 'Luca', { owner: 'Luca' }],
|
|
|
|
|
['cadence', 'Weekly', { cadence: 'Weekly' }],
|
|
|
|
|
['archived', true, { archived: true }],
|
|
|
|
|
['trashed', true, { trashed: true }],
|
|
|
|
|
['order', 5, { order: 5 }],
|
|
|
|
|
] as [string, unknown, Partial<VaultEntry>][])(
|
|
|
|
|
'maps %s update to correct entry field',
|
|
|
|
|
(key, value, expected) => {
|
|
|
|
|
expect(frontmatterToEntryPatch('update', key, value as never)).toEqual(expected)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
it('maps aliases update with array value', () => {
|
|
|
|
|
expect(frontmatterToEntryPatch('update', 'aliases', ['A', 'B'])).toEqual({ aliases: ['A', 'B'] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('maps belongs_to update with array value', () => {
|
|
|
|
|
expect(frontmatterToEntryPatch('update', 'belongs_to', ['[[parent]]'])).toEqual({ belongsTo: ['[[parent]]'] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handles case-insensitive keys with spaces (e.g. "Is A", "Belongs to")', () => {
|
|
|
|
|
expect(frontmatterToEntryPatch('update', 'Is A', 'Experiment')).toEqual({ isA: 'Experiment' })
|
|
|
|
|
expect(frontmatterToEntryPatch('update', 'Belongs to', ['[[x]]'])).toEqual({ belongsTo: ['[[x]]'] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('returns empty object for unknown keys', () => {
|
|
|
|
|
expect(frontmatterToEntryPatch('update', 'custom_field', 'value')).toEqual({})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['status', { status: null }],
|
|
|
|
|
['color', { color: null }],
|
|
|
|
|
['aliases', { aliases: [] }],
|
|
|
|
|
['archived', { archived: false }],
|
|
|
|
|
['order', { order: null }],
|
|
|
|
|
] as [string, Partial<VaultEntry>][])(
|
|
|
|
|
'maps delete of %s to null/default',
|
|
|
|
|
(key, expected) => {
|
|
|
|
|
expect(frontmatterToEntryPatch('delete', key)).toEqual(expected)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
it('returns empty object for unknown key on delete', () => {
|
|
|
|
|
expect(frontmatterToEntryPatch('delete', 'unknown_key')).toEqual({})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
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
|
|
|
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()
|
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 updateContent = 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 => ({
|
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
|
|
|
addEntry, removeEntry, updateContent, entries, setToastMessage, updateEntry,
|
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)
|
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('handleCreateNote calls addEntry and creates correct entry', () => {
|
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.handleCreateNote('Test Note', 'Note')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
const [createdEntry, createdContent] = addEntry.mock.calls[0]
|
|
|
|
|
expect(createdEntry.title).toBe('Test Note')
|
|
|
|
|
expect(createdEntry.isA).toBe('Note')
|
|
|
|
|
expect(createdEntry.path).toContain('note/test-note.md')
|
|
|
|
|
expect(createdContent).toContain('title: Test Note')
|
|
|
|
|
})
|
|
|
|
|
|
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')
|
|
|
|
|
expect(result.current.activeTabPath).toContain('note/fast-note.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('handleCreateType creates type entry', () => {
|
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.handleCreateType('Recipe')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
const [createdEntry] = addEntry.mock.calls[0]
|
|
|
|
|
expect(createdEntry.isA).toBe('Type')
|
|
|
|
|
expect(createdEntry.title).toBe('Recipe')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleNavigateWikilink finds entry by title', async () => {
|
|
|
|
|
const target = makeEntry({ title: 'Target Note', path: '/vault/note/target.md' })
|
|
|
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/note/target.md')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(updateContent).toHaveBeenCalled()
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(updateContent).toHaveBeenCalled()
|
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-02-24 23:03:01 +01:00
|
|
|
it('handleCreateNoteImmediate creates note with auto-generated title', () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
const [createdEntry] = addEntry.mock.calls[0]
|
|
|
|
|
expect(createdEntry.title).toBe('Untitled note')
|
|
|
|
|
expect(createdEntry.isA).toBe('Note')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleCreateNoteImmediate generates unique names on rapid calls', () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(3)
|
|
|
|
|
const titles = addEntry.mock.calls.map(([e]: [VaultEntry]) => e.title)
|
|
|
|
|
expect(titles[0]).toBe('Untitled note')
|
|
|
|
|
expect(titles[1]).toBe('Untitled note 2')
|
|
|
|
|
expect(titles[2]).toBe('Untitled note 3')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleCreateNoteImmediate accepts custom type', () => {
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.handleCreateNoteImmediate('Project')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
const [createdEntry] = addEntry.mock.calls[0]
|
|
|
|
|
expect(createdEntry.title).toBe('Untitled project')
|
|
|
|
|
expect(createdEntry.isA).toBe('Project')
|
|
|
|
|
})
|
|
|
|
|
|
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', () => {
|
|
|
|
|
it('createAndPersist calls addPendingSave on start (non-Tauri)', async () => {
|
|
|
|
|
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('Pending Test', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addPendingSave).toHaveBeenCalledWith(expect.stringContaining('note/pending-test.md'))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('createAndPersist calls removePendingSave when persist completes (non-Tauri)', async () => {
|
|
|
|
|
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('Persist OK', 'Note')
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(removePendingSave).toHaveBeenCalledWith(expect.stringContaining('note/persist-ok.md'))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addPendingSave).toHaveBeenCalledWith(expect.stringContaining('note/fail-save.md'))
|
|
|
|
|
expect(removePendingSave).toHaveBeenCalledWith(expect.stringContaining('note/fail-save.md'))
|
|
|
|
|
expect(removeEntry).toHaveBeenCalledWith(expect.stringContaining('note/fail-save.md'))
|
|
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Failed to create note — disk write error')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handleCreateNoteImmediate works with pending save callbacks', async () => {
|
|
|
|
|
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.handleCreateNoteImmediate()
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addPendingSave).toHaveBeenCalledWith(expect.stringContaining('note/untitled-note.md'))
|
|
|
|
|
expect(removePendingSave).toHaveBeenCalledWith(expect.stringContaining('note/untitled-note.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
|
|
|
describe('optimistic error recovery (Tauri mode)', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['handleCreateNote', 'Failing Note', 'Note', 'note/failing-note.md'],
|
|
|
|
|
['handleCreateType', 'Recipe', 'Type', 'type/recipe.md'],
|
|
|
|
|
])('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))
|
|
|
|
|
expect(setToastMessage).toHaveBeenCalledWith('Failed to create note — disk write error')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('handles rapid creation with one failure independently', async () => {
|
|
|
|
|
vi.mocked(invoke)
|
|
|
|
|
.mockResolvedValueOnce(undefined)
|
|
|
|
|
.mockRejectedValueOnce(new Error('disk full'))
|
|
|
|
|
.mockResolvedValueOnce(undefined)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
result.current.handleCreateNoteImmediate()
|
|
|
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(addEntry).toHaveBeenCalledTimes(3)
|
|
|
|
|
expect(removeEntry).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(removeEntry).toHaveBeenCalledWith(expect.stringContaining('untitled-note-2.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
|
|
|
})
|