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'
|
2026-04-24 23:29:44 +02:00
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
|
|
|
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-24 21:53:47 +02:00
|
|
|
import {
|
|
|
|
|
useTabManagement,
|
|
|
|
|
prefetchNoteContent,
|
|
|
|
|
cacheNoteContent,
|
|
|
|
|
clearPrefetchCache,
|
|
|
|
|
NOTE_CONTENT_CACHE_MAX_BYTES,
|
|
|
|
|
NOTE_CONTENT_ENTRY_MAX_BYTES,
|
2026-05-05 19:11:17 +02:00
|
|
|
NOTE_CONTENT_PREFETCH_CONCURRENCY,
|
2026-04-24 21:53:47 +02:00
|
|
|
} from './useTabManagement'
|
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', () => ({
|
2026-04-24 23:29:44 +02: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
|
|
|
mockInvoke: vi.fn().mockResolvedValue('# Mock content'),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
|
|
|
|
|
path: '/vault/note/test.md',
|
|
|
|
|
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-03-03 11:22:04 +01:00
|
|
|
template: null, sort: 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,
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
type HookState = { current: ReturnType<typeof useTabManagement> }
|
|
|
|
|
|
|
|
|
|
async function selectNote(result: HookState, overrides: Partial<VaultEntry>) {
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleSelectNote(makeEntry(overrides))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 10:29:22 +02:00
|
|
|
async function selectNoteWithTimers(result: HookState, overrides: Partial<VaultEntry>, advanceMs: number) {
|
|
|
|
|
let openPromise!: Promise<void>
|
|
|
|
|
act(() => {
|
|
|
|
|
openPromise = result.current.handleSelectNote(makeEntry(overrides))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await vi.advanceTimersByTimeAsync(advanceMs)
|
|
|
|
|
await openPromise
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function withFakeTimers(run: () => Promise<void>) {
|
|
|
|
|
vi.useFakeTimers()
|
|
|
|
|
try {
|
|
|
|
|
await run()
|
|
|
|
|
} finally {
|
|
|
|
|
vi.useRealTimers()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
async function replaceActiveNote(result: HookState, overrides: Partial<VaultEntry>) {
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleReplaceActiveTab(makeEntry(overrides))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 23:14:36 +02:00
|
|
|
async function prefetchResolvedContent(path: string, content: string, entry?: VaultEntry) {
|
2026-04-29 10:08:33 +02:00
|
|
|
mockNoteContent({ [path]: content })
|
2026-05-01 23:14:36 +02:00
|
|
|
prefetchNoteContent(entry ?? path)
|
2026-04-18 09:19:29 +02:00
|
|
|
await vi.waitFor(() => expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1))
|
|
|
|
|
return mockInvoke
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 10:08:33 +02:00
|
|
|
function mockNoteContent(contentByPath: Record<string, string>) {
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((cmd: string, args?: Record<string, unknown>) => {
|
|
|
|
|
const path = typeof args?.path === 'string' ? args.path : ''
|
|
|
|
|
const content = contentByPath[path] ?? '# Mock content'
|
|
|
|
|
if (cmd === 'validate_note_content') {
|
|
|
|
|
return Promise.resolve(content === args?.content)
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve(content)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
function expectSingleActiveTab(result: HookState, path: string) {
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].entry.path).toBe(path)
|
|
|
|
|
expect(result.current.activeTabPath).toBe(path)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
function createDeferred<T>() {
|
|
|
|
|
let resolve!: (value: T) => void
|
|
|
|
|
const promise = new Promise<T>((res) => {
|
|
|
|
|
resolve = res
|
|
|
|
|
})
|
|
|
|
|
return { promise, resolve }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 21:53:47 +02:00
|
|
|
function makeAsciiContent(byteCount: number): string {
|
|
|
|
|
return 'x'.repeat(byteCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function seedCacheBeyondByteLimit() {
|
|
|
|
|
const cachedContent = makeAsciiContent(Math.floor(NOTE_CONTENT_ENTRY_MAX_BYTES * 0.9))
|
|
|
|
|
const cachedPaths = Array.from(
|
|
|
|
|
{ length: Math.floor(NOTE_CONTENT_CACHE_MAX_BYTES / cachedContent.length) + 2 },
|
|
|
|
|
(_, index) => `/vault/note/cached-${index + 1}.md`,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (const path of cachedPaths) {
|
|
|
|
|
cacheNoteContent(path, cachedContent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
cachedContent,
|
|
|
|
|
oldestPath: cachedPaths[0],
|
|
|
|
|
newestPath: cachedPaths[cachedPaths.length - 1],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
describe('useTabManagement (single-note model)', () => {
|
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(() => {
|
2026-04-24 23:29:44 +02:00
|
|
|
vi.resetAllMocks()
|
2026-04-18 09:19:29 +02:00
|
|
|
clearPrefetchCache()
|
2026-04-24 23:29:44 +02:00
|
|
|
vi.mocked(isTauri).mockReturnValue(false)
|
|
|
|
|
vi.mocked(mockInvoke).mockResolvedValue('# Mock content')
|
|
|
|
|
window.history.replaceState({}, '', '/')
|
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
|
|
|
})
|
|
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('starts with no note and null active path', () => {
|
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 { result } = renderHook(() => useTabManagement())
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('handleSelectNote', () => {
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('opens a note and sets it active', async () => {
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/note/a.md' })
|
|
|
|
|
expectSingleActiveTab(result, '/vault/note/a.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
|
|
|
|
2026-05-07 06:41:50 +02:00
|
|
|
it('normalizes partially hydrated note metadata before opening after reload churn', async () => {
|
|
|
|
|
const partialEntry = {
|
|
|
|
|
path: '/vault/note/apple-mail.md',
|
|
|
|
|
title: undefined,
|
|
|
|
|
filename: undefined,
|
|
|
|
|
aliases: undefined,
|
|
|
|
|
outgoingLinks: undefined,
|
|
|
|
|
} as unknown as VaultEntry
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleSelectNote(partialEntry)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expectSingleActiveTab(result, '/vault/note/apple-mail.md')
|
|
|
|
|
expect(result.current.tabs[0].entry).toEqual(expect.objectContaining({
|
|
|
|
|
filename: 'apple-mail.md',
|
|
|
|
|
title: 'apple-mail',
|
|
|
|
|
aliases: [],
|
|
|
|
|
outgoingLinks: [],
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ignores note-open requests without a usable path', async () => {
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleSelectNote(makeEntry({ path: ' ' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(vi.mocked(mockInvoke)).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
it('switches the active path immediately while the next note is still loading', async () => {
|
|
|
|
|
let resolveContent: (value: string) => void
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementationOnce(
|
|
|
|
|
() => new Promise<string>((resolve) => { resolveContent = resolve }),
|
|
|
|
|
)
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
void act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/vault/note/pending.md', title: 'Pending' }))
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
})
|
2026-04-09 12:20:03 +02:00
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/note/pending.md')
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
await act(async () => {
|
2026-04-09 12:20:03 +02:00
|
|
|
resolveContent!('# Pending content')
|
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
|
|
|
})
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
expect(result.current.tabs[0].entry.path).toBe('/vault/note/pending.md')
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Pending content')
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
})
|
|
|
|
|
|
2026-05-07 00:13:52 +02:00
|
|
|
it('does not reopen a stale note load after all tabs are closed', async () => {
|
|
|
|
|
const content = createDeferred<string>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementationOnce(() => content.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
void act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/old-vault/note/pending.md', title: 'Pending' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.closeAllTabs()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
content.resolve('# Stale content')
|
|
|
|
|
await content.promise
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-09 12:20:03 +02:00
|
|
|
it('replaces the current note when selecting a different one', async () => {
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
const { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
await selectNote(result, { path: '/vault/b.md', title: 'B' })
|
|
|
|
|
expectSingleActiveTab(result, '/vault/b.md')
|
|
|
|
|
})
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
|
2026-04-29 11:42:37 +02:00
|
|
|
it('keeps a dirty already-open note in place when selecting it again', async () => {
|
2026-04-09 12:20:03 +02:00
|
|
|
const entry = { path: '/vault/a.md' }
|
2026-04-29 11:42:37 +02:00
|
|
|
const { result: dirtyResult } = renderHook(() => useTabManagement({
|
|
|
|
|
hasUnsavedChanges: (path) => path === '/vault/a.md',
|
|
|
|
|
}))
|
|
|
|
|
await selectNote(dirtyResult, entry)
|
|
|
|
|
act(() => {
|
|
|
|
|
dirtyResult.current.setTabs(prev => prev.map(tab =>
|
|
|
|
|
tab.entry.path === entry.path ? { ...tab, content: '# Local draft' } : tab
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
|
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 () => {
|
2026-04-29 11:42:37 +02:00
|
|
|
await dirtyResult.current.handleSelectNote(makeEntry(entry))
|
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-29 11:42:37 +02:00
|
|
|
expect(dirtyResult.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(dirtyResult.current.tabs[0].content).toBe('# Local draft')
|
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-05-13 10:29:22 +02:00
|
|
|
it('retries transient note content load failures before opening the note', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockRejectedValueOnce(new Error('transient IPC failure'))
|
|
|
|
|
.mockResolvedValueOnce('# Recovered content')
|
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 warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
|
2026-05-13 10:29:22 +02:00
|
|
|
try {
|
|
|
|
|
await withFakeTimers(async () => {
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNoteWithTimers(result, { path: '/vault/note/recovered.md' }, 120)
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Recovered content')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
|
|
|
|
expect(warnSpy).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
} finally {
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
}
|
|
|
|
|
})
|
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-05-13 10:29:22 +02:00
|
|
|
it('does not display a note as empty when content loading fails', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockRejectedValue(new Error('transient IPC failure'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await withFakeTimers(async () => {
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNoteWithTimers(result, { path: '/vault/note/failing.md' }, 1_240)
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(warnSpy).toHaveBeenCalledWith('Failed to load note content:', expect.any(Error))
|
|
|
|
|
})
|
|
|
|
|
} finally {
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
}
|
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 20:18:26 +02:00
|
|
|
|
|
|
|
|
it('clears the active note when the file is missing on disk', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('File does not exist: /vault/note/missing.md'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
const onMissingNotePath = vi.fn()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ onMissingNotePath }))
|
|
|
|
|
await selectNote(result, { path: '/vault/note/missing.md', title: 'Missing Note' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(onMissingNotePath).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ path: '/vault/note/missing.md', title: 'Missing Note' }),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
2026-04-24 02:16:49 +02:00
|
|
|
|
2026-04-24 03:02:03 +02:00
|
|
|
it('returns to the empty state when note content is not valid UTF-8 text', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('File is not valid UTF-8 text: /vault/note/bad.csv'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
const onUnreadableNoteContent = vi.fn()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ onUnreadableNoteContent }))
|
|
|
|
|
await selectNote(result, {
|
|
|
|
|
path: '/vault/note/bad.csv',
|
|
|
|
|
filename: 'bad.csv',
|
|
|
|
|
title: 'bad.csv',
|
|
|
|
|
fileKind: 'text',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(onUnreadableNoteContent).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ path: '/vault/note/bad.csv', title: 'bad.csv' }),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-27 00:36:24 +02:00
|
|
|
it('opens binary image files without trying to load them as text notes', async () => {
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, {
|
|
|
|
|
path: '/vault/assets/photo.png',
|
|
|
|
|
filename: 'photo.png',
|
|
|
|
|
title: 'photo.png',
|
|
|
|
|
fileKind: 'binary',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expectSingleActiveTab(result, '/vault/assets/photo.png')
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 02:16:49 +02:00
|
|
|
it('returns to the empty state when no active vault is selected', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('No active vault selected'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/note/orphaned.md', title: 'Orphaned Note' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
2026-04-24 23:29:44 +02:00
|
|
|
|
2026-05-02 17:40:58 +02:00
|
|
|
it('reports an unavailable active vault instead of opening a blank stale tab', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('Active vault is not available'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
const onMissingActiveVault = vi.fn()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ onMissingActiveVault }))
|
|
|
|
|
await selectNote(result, { path: '/vault/note/orphaned.md', title: 'Orphaned Note' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(onMissingActiveVault).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ path: '/vault/note/orphaned.md', title: 'Orphaned Note' }),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 23:29:44 +02:00
|
|
|
it('uses the note-window vault path when Tauri reloads the selected note', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockResolvedValue('# Window content')
|
|
|
|
|
window.history.replaceState(
|
|
|
|
|
{},
|
|
|
|
|
'',
|
|
|
|
|
'/?window=note&path=%2Fvault%2Fnote%2Ftest.md&vault=%2Fvault&title=Test+Note',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/note/test.md', title: 'Test Note' })
|
|
|
|
|
|
|
|
|
|
expect(vi.mocked(invoke)).toHaveBeenCalledWith('get_note_content', {
|
|
|
|
|
path: '/vault/note/test.md',
|
|
|
|
|
vaultPath: '/vault',
|
|
|
|
|
})
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Window content')
|
|
|
|
|
})
|
2026-05-11 16:37:06 +02:00
|
|
|
|
|
|
|
|
it('uses the entry workspace vault path when Tauri opens a mounted workspace note', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockResolvedValue('# Workspace content')
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, {
|
|
|
|
|
path: '/team/tolaria-app.md',
|
|
|
|
|
title: 'Tolaria',
|
|
|
|
|
workspace: {
|
|
|
|
|
id: 'team',
|
|
|
|
|
label: 'Team',
|
|
|
|
|
alias: 'team',
|
|
|
|
|
path: '/team',
|
|
|
|
|
shortLabel: 'TE',
|
|
|
|
|
color: null,
|
|
|
|
|
icon: null,
|
|
|
|
|
mounted: true,
|
|
|
|
|
available: true,
|
|
|
|
|
defaultForNewNotes: false,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(vi.mocked(invoke)).toHaveBeenCalledWith('get_note_content', {
|
|
|
|
|
path: '/team/tolaria-app.md',
|
|
|
|
|
vaultPath: '/team',
|
|
|
|
|
})
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Workspace content')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not reuse cached content across workspaces with the same relative note path', async () => {
|
|
|
|
|
vi.mocked(isTauri).mockReturnValue(true)
|
|
|
|
|
vi.mocked(invoke).mockImplementation((_cmd: string, args?: { vaultPath?: string }) => (
|
|
|
|
|
Promise.resolve(args?.vaultPath === '/team' ? '# Team content' : '# Personal content')
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
const personalWorkspace = {
|
|
|
|
|
id: 'personal',
|
|
|
|
|
label: 'Personal',
|
|
|
|
|
alias: 'personal',
|
|
|
|
|
path: '/personal',
|
|
|
|
|
shortLabel: 'PE',
|
|
|
|
|
color: null,
|
|
|
|
|
icon: null,
|
|
|
|
|
mounted: true,
|
|
|
|
|
available: true,
|
|
|
|
|
defaultForNewNotes: false,
|
|
|
|
|
}
|
|
|
|
|
const teamWorkspace = { ...personalWorkspace, id: 'team', label: 'Team', alias: 'team', path: '/team', shortLabel: 'TE' }
|
|
|
|
|
prefetchNoteContent(makeEntry({ path: 'shared.md', workspace: personalWorkspace }))
|
|
|
|
|
await vi.waitFor(() => expect(vi.mocked(invoke)).toHaveBeenCalledWith('get_note_content', {
|
|
|
|
|
path: 'shared.md',
|
|
|
|
|
vaultPath: '/personal',
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, {
|
|
|
|
|
path: 'shared.md',
|
|
|
|
|
title: 'Shared',
|
|
|
|
|
workspace: teamWorkspace,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(vi.mocked(invoke)).toHaveBeenCalledWith('get_note_content', {
|
|
|
|
|
path: 'shared.md',
|
|
|
|
|
vaultPath: '/team',
|
|
|
|
|
})
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Team content')
|
|
|
|
|
})
|
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('handleReplaceActiveTab', () => {
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('replaces the current note with a new entry', async () => {
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
await replaceActiveNote(result, { path: '/vault/b.md', title: 'B' })
|
|
|
|
|
expectSingleActiveTab(result, '/vault/b.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
|
|
|
})
|
|
|
|
|
|
2026-04-20 14:43:52 +02:00
|
|
|
it('treats /tmp and /private/tmp aliases as the same active note', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockResolvedValueOnce('# Stale before pull')
|
|
|
|
|
.mockResolvedValueOnce('# Fresh after pull')
|
|
|
|
|
const beforeNavigate = vi.fn().mockResolvedValue(undefined)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ beforeNavigate }))
|
|
|
|
|
await selectNote(result, { path: '/private/tmp/vault/active.md', title: 'Active' })
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleReplaceActiveTab(
|
|
|
|
|
makeEntry({ path: '/tmp/vault/active.md', title: 'Active' }),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(beforeNavigate).not.toHaveBeenCalled()
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/tmp/vault/active.md')
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Fresh after pull')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('reloads content when replacing with the same entry', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockResolvedValueOnce('# Stale before pull')
|
|
|
|
|
.mockResolvedValueOnce('# Fresh after pull')
|
|
|
|
|
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-20 14:43:52 +02:00
|
|
|
const entry = { path: '/vault/a.md', title: 'A' }
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, entry)
|
2026-04-20 14:43:52 +02: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
|
|
|
await act(async () => {
|
2026-04-09 12:20:03 +02:00
|
|
|
await result.current.handleReplaceActiveTab(makeEntry(entry))
|
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(result.current.tabs).toHaveLength(1)
|
2026-04-20 14:43:52 +02:00
|
|
|
expect(result.current.tabs[0].content).toBe('# Fresh after pull')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
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 20:18:26 +02:00
|
|
|
it('clears the active note when a forced reload hits a missing file path', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockResolvedValueOnce('# Existing content')
|
|
|
|
|
.mockRejectedValueOnce(new Error('File does not exist: /vault/a.md'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
const onMissingNotePath = vi.fn()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ onMissingNotePath }))
|
|
|
|
|
const entry = makeEntry({ path: '/vault/a.md', title: 'A' })
|
|
|
|
|
await selectNote(result, entry)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleReplaceActiveTab(entry)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(onMissingNotePath).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ path: '/vault/a.md', title: 'A' }),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('opens a note when no note is active', async () => {
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await replaceActiveNote(result, { path: '/vault/a.md' })
|
|
|
|
|
expectSingleActiveTab(result, '/vault/a.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
|
|
|
})
|
2026-05-01 23:14:36 +02:00
|
|
|
|
|
|
|
|
it('validates cached content before replacing with a different active note', async () => {
|
|
|
|
|
cacheNoteContent('/vault/b.md', '# Stale cached B')
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((cmd: string) => {
|
|
|
|
|
if (cmd === 'validate_note_content') return Promise.resolve(false)
|
|
|
|
|
return Promise.resolve('# Fresh disk B')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
await replaceActiveNote(result, { path: '/vault/b.md', title: 'B' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Fresh disk B')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledWith('validate_note_content', {
|
|
|
|
|
path: '/vault/b.md',
|
|
|
|
|
content: '# Stale cached B',
|
|
|
|
|
})
|
|
|
|
|
})
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
})
|
2026-02-23 20:12:54 +01:00
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
describe('openTabWithContent', () => {
|
|
|
|
|
it('opens a note with pre-loaded content', () => {
|
2026-02-23 20:12:54 +01:00
|
|
|
const { result } = renderHook(() => useTabManagement())
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
const entry = makeEntry({ path: '/vault/new.md' })
|
2026-02-23 20:12:54 +01:00
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
act(() => {
|
|
|
|
|
result.current.openTabWithContent(entry, '# New note')
|
2026-02-23 20:12:54 +01:00
|
|
|
})
|
|
|
|
|
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# New note')
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/new.md')
|
2026-02-23 20:12:54 +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
|
|
|
})
|
|
|
|
|
|
2026-03-03 19:33:12 +01:00
|
|
|
describe('setTabs entry sync', () => {
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('updates note entry via setTabs mapper (vault entry sync pattern)', async () => {
|
2026-03-03 19:33:12 +01:00
|
|
|
const { result } = renderHook(() => useTabManagement())
|
2026-04-06 12:21:56 +02:00
|
|
|
const entry = makeEntry({ path: '/vault/a.md', archived: false })
|
2026-03-03 19:33:12 +01:00
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleSelectNote(entry)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-06 12:21:56 +02:00
|
|
|
const freshEntry = { ...entry, archived: true }
|
2026-03-03 19:33:12 +01:00
|
|
|
act(() => {
|
|
|
|
|
result.current.setTabs(prev => prev.map(tab =>
|
|
|
|
|
tab.entry.path === freshEntry.path ? { ...tab, entry: freshEntry } : tab
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-06 12:21:56 +02:00
|
|
|
expect(result.current.tabs[0].entry.archived).toBe(true)
|
2026-03-03 19:33:12 +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
|
|
|
describe('closeAllTabs', () => {
|
refactor: remove tab bar — single note open at a time
Replace the multi-tab model with single-note navigation. One note is
open at a time; clicking any note (sidebar, wikilink, relationship)
replaces the current note in the editor. Back/Forward history still
works via Cmd+[/].
Removed: TabBar component, useClosedTabHistory, tab reorder/close/
reopen logic, Cmd+W/Cmd+Shift+T shortcuts, Close Tab and Reopen
Closed Tab menu items, tabLayout utility, and all related tests.
Simplified: useTabManagement (single-note), useAppNavigation (no tab
lookup), useKeyboardNavigation (note nav only), useNoteCreation (no
close-tab cleanup), useDeleteActions (deselect instead of close tab),
Editor (no TabBar render), Rust menu (removed 2 menu items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:24:49 +01:00
|
|
|
it('clears the note and active path', async () => {
|
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 { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/a.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
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.closeAllTabs()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toHaveLength(0)
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
describe('content prefetch cache', () => {
|
2026-04-29 10:08:33 +02:00
|
|
|
it('prefetch validates cached content against disk before reuse', async () => {
|
2026-04-18 09:19:29 +02:00
|
|
|
const mockInvoke = await prefetchResolvedContent('/vault/note/pre.md', '# Prefetched content')
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/note/pre.md', title: 'Pre' })
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Prefetched content')
|
2026-04-22 20:18:26 +02:00
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
2026-04-29 10:08:33 +02:00
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenLastCalledWith('validate_note_content', {
|
|
|
|
|
path: '/vault/note/pre.md',
|
|
|
|
|
content: '# Prefetched content',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-01 23:14:36 +02:00
|
|
|
it('uses identity-matched prefetched content without re-reading the file', async () => {
|
|
|
|
|
const entry = makeEntry({
|
|
|
|
|
path: '/vault/note/pre.md',
|
|
|
|
|
modifiedAt: 1700000001,
|
|
|
|
|
fileSize: 19,
|
|
|
|
|
})
|
|
|
|
|
const mockInvoke = await prefetchResolvedContent(entry.path, '# Prefetched content', entry)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, entry)
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Prefetched content')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-29 10:08:33 +02:00
|
|
|
it('does not paint cached content until freshness validation passes', async () => {
|
|
|
|
|
const freshness = createDeferred<boolean>()
|
|
|
|
|
cacheNoteContent('/vault/note/stale.md', '# Stale cached content')
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((cmd: string) => {
|
|
|
|
|
if (cmd === 'validate_note_content') return freshness.promise
|
|
|
|
|
return Promise.resolve('# Fresh disk content')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/vault/note/stale.md', title: 'Stale' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/note/stale.md')
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
freshness.resolve(false)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Fresh disk content')
|
|
|
|
|
})
|
2026-03-09 13:07:31 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('clearPrefetchCache prevents stale content from being served', async () => {
|
2026-04-18 09:19:29 +02:00
|
|
|
const mockInvoke = await prefetchResolvedContent('/vault/note/stale.md', '# Stale')
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
clearPrefetchCache()
|
2026-04-29 10:08:33 +02:00
|
|
|
mockNoteContent({ '/vault/note/stale.md': '# Fresh' })
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
2026-04-09 12:20:03 +02:00
|
|
|
await selectNote(result, { path: '/vault/note/stale.md', title: 'Stale' })
|
2026-03-09 13:07:31 +01:00
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Fresh')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('deduplicates concurrent prefetch requests for same path', async () => {
|
|
|
|
|
vi.mocked(mockInvoke).mockResolvedValue('# Content')
|
|
|
|
|
|
|
|
|
|
prefetchNoteContent('/vault/note/dup.md')
|
|
|
|
|
prefetchNoteContent('/vault/note/dup.md')
|
|
|
|
|
prefetchNoteContent('/vault/note/dup.md')
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1))
|
|
|
|
|
})
|
2026-04-18 09:19:29 +02:00
|
|
|
|
2026-05-05 19:11:17 +02:00
|
|
|
it('limits concurrent background prefetch reads', async () => {
|
|
|
|
|
const pendingReads = new Map<string, ReturnType<typeof createDeferred<string>>>()
|
|
|
|
|
let activeReads = 0
|
|
|
|
|
let maxActiveReads = 0
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((_cmd: string, args?: Record<string, unknown>) => {
|
|
|
|
|
const path = typeof args?.path === 'string' ? args.path : ''
|
|
|
|
|
const deferred = createDeferred<string>()
|
|
|
|
|
pendingReads.set(path, deferred)
|
|
|
|
|
activeReads += 1
|
|
|
|
|
maxActiveReads = Math.max(maxActiveReads, activeReads)
|
|
|
|
|
return deferred.promise.finally(() => {
|
|
|
|
|
activeReads -= 1
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const paths = Array.from({ length: NOTE_CONTENT_PREFETCH_CONCURRENCY + 3 }, (_, index) => `/vault/note/pre-${index}.md`)
|
|
|
|
|
for (const path of paths) prefetchNoteContent(path)
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(NOTE_CONTENT_PREFETCH_CONCURRENCY)
|
|
|
|
|
})
|
|
|
|
|
expect(maxActiveReads).toBe(NOTE_CONTENT_PREFETCH_CONCURRENCY)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
pendingReads.get(paths[0])?.resolve('# First')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(NOTE_CONTENT_PREFETCH_CONCURRENCY + 1)
|
|
|
|
|
})
|
|
|
|
|
expect(maxActiveReads).toBe(NOTE_CONTENT_PREFETCH_CONCURRENCY)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
for (const deferred of pendingReads.values()) deferred.resolve('# Done')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(paths.length)
|
|
|
|
|
})
|
|
|
|
|
await act(async () => {
|
|
|
|
|
for (const deferred of pendingReads.values()) deferred.resolve('# Done')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('promotes a queued prefetch when the note is opened', async () => {
|
|
|
|
|
const pendingReads = new Map<string, ReturnType<typeof createDeferred<string>>>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((_cmd: string, args?: Record<string, unknown>) => {
|
|
|
|
|
const path = typeof args?.path === 'string' ? args.path : ''
|
|
|
|
|
const deferred = createDeferred<string>()
|
|
|
|
|
pendingReads.set(path, deferred)
|
|
|
|
|
return deferred.promise
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const paths = Array.from({ length: NOTE_CONTENT_PREFETCH_CONCURRENCY + 1 }, (_, index) => `/vault/note/promote-${index}.md`)
|
|
|
|
|
for (const path of paths) prefetchNoteContent(path)
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(NOTE_CONTENT_PREFETCH_CONCURRENCY)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const queuedPath = paths[NOTE_CONTENT_PREFETCH_CONCURRENCY]
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await act(async () => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: queuedPath, title: 'Queued' }))
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(pendingReads.has(queuedPath)).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
pendingReads.get(queuedPath)?.resolve('# Queued content')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Queued content')
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
for (const [path, deferred] of pendingReads) {
|
|
|
|
|
if (path !== queuedPath) deferred.resolve('# Done')
|
|
|
|
|
}
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-24 02:16:49 +02:00
|
|
|
it('swallows no-active-vault prefetch failures and lets a later open recover', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockRejectedValueOnce(new Error('No active vault selected'))
|
|
|
|
|
.mockResolvedValueOnce('# Recovered content')
|
|
|
|
|
|
|
|
|
|
prefetchNoteContent('/vault/note/recovered.md')
|
|
|
|
|
await vi.waitFor(() => expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1))
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/note/recovered.md', title: 'Recovered' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Recovered content')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
it('serves refreshed cached content after a save replaces stale prefetched data', async () => {
|
|
|
|
|
const mockInvoke = await prefetchResolvedContent('/vault/note/saved.md', '# Stale prefetched content')
|
2026-04-29 10:08:33 +02:00
|
|
|
mockNoteContent({ '/vault/note/saved.md': '# Persisted content' })
|
2026-04-18 09:19:29 +02:00
|
|
|
|
|
|
|
|
cacheNoteContent('/vault/note/saved.md', '# Persisted content')
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/note/saved.md', title: 'Saved' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Persisted content')
|
2026-04-22 20:18:26 +02:00
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
2026-04-19 11:29:04 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-29 10:08:33 +02:00
|
|
|
it('activates a warmed note after validating cached content', async () => {
|
|
|
|
|
const deferred = createDeferred<boolean>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((cmd: string) => {
|
|
|
|
|
if (cmd === 'validate_note_content') return deferred.promise
|
|
|
|
|
return Promise.resolve('# Warm content')
|
|
|
|
|
})
|
2026-04-19 11:29:04 +02:00
|
|
|
cacheNoteContent('/vault/note/warm.md', '# Warm content')
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/vault/note/warm.md', title: 'Warm' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/note/warm.md')
|
2026-04-29 10:08:33 +02:00
|
|
|
expect(result.current.tabs).toEqual([])
|
2026-04-22 20:18:26 +02:00
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-04-29 10:08:33 +02:00
|
|
|
deferred.resolve(true)
|
2026-04-22 20:18:26 +02:00
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
2026-04-29 10:08:33 +02:00
|
|
|
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Warm content')
|
2026-04-18 09:19:29 +02:00
|
|
|
})
|
2026-04-18 17:30:49 +02:00
|
|
|
|
2026-04-24 21:53:47 +02:00
|
|
|
it('does not retain oversized notes in the prefetch cache', async () => {
|
|
|
|
|
const largeContent = makeAsciiContent(NOTE_CONTENT_ENTRY_MAX_BYTES + 1)
|
|
|
|
|
const mockInvoke = await prefetchResolvedContent('/vault/note/oversized.md', largeContent)
|
|
|
|
|
const deferred = createDeferred<string>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementationOnce(() => deferred.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/vault/note/oversized.md', title: 'Oversized' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/note/oversized.md')
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
deferred.resolve(largeContent)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe(largeContent)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('evicts the oldest cached notes when retained bytes exceed the cache budget', async () => {
|
|
|
|
|
const { cachedContent, oldestPath } = seedCacheBeyondByteLimit()
|
|
|
|
|
const deferred = createDeferred<string>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementationOnce(() => deferred.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: oldestPath, title: 'Oldest cached note' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe(oldestPath)
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
deferred.resolve(cachedContent)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe(cachedContent)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the newest cached notes warm when trimming to the byte budget', async () => {
|
|
|
|
|
const { cachedContent, newestPath } = seedCacheBeyondByteLimit()
|
2026-04-29 10:08:33 +02:00
|
|
|
const deferred = createDeferred<boolean>()
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementation((cmd: string) => {
|
|
|
|
|
if (cmd === 'validate_note_content') return deferred.promise
|
|
|
|
|
return Promise.resolve(cachedContent)
|
|
|
|
|
})
|
2026-04-24 21:53:47 +02:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: newestPath, title: 'Newest cached note' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe(newestPath)
|
2026-04-29 10:08:33 +02:00
|
|
|
expect(result.current.tabs).toEqual([])
|
2026-04-24 21:53:47 +02:00
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-04-29 10:08:33 +02:00
|
|
|
deferred.resolve(true)
|
2026-04-24 21:53:47 +02:00
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
2026-04-29 10:08:33 +02:00
|
|
|
|
|
|
|
|
expect(result.current.tabs).toHaveLength(1)
|
|
|
|
|
expect(result.current.tabs[0].content).toBe(cachedContent)
|
2026-04-24 21:53:47 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-18 17:30:49 +02:00
|
|
|
it('reuses cached content when reopening a recently loaded note', async () => {
|
2026-04-29 10:08:33 +02:00
|
|
|
mockNoteContent({
|
|
|
|
|
'/vault/a.md': '# A content',
|
|
|
|
|
'/vault/b.md': '# B content',
|
|
|
|
|
})
|
2026-04-18 17:30:49 +02:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
await selectNote(result, { path: '/vault/b.md', title: 'B' })
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A again' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].entry.path).toBe('/vault/a.md')
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# A content')
|
2026-05-01 23:14:36 +02:00
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2)
|
2026-04-22 20:18:26 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-29 11:42:37 +02:00
|
|
|
it('refreshes an already-open clean note when cached content is stale on disk', async () => {
|
|
|
|
|
mockNoteContent({ '/vault/a.md': '# Original content' })
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
|
|
|
|
|
mockNoteContent({ '/vault/a.md': '# External edit' })
|
2026-05-01 23:14:36 +02:00
|
|
|
await selectNote(result, {
|
|
|
|
|
path: '/vault/a.md',
|
|
|
|
|
title: 'A',
|
|
|
|
|
modifiedAt: 1700000001,
|
|
|
|
|
fileSize: 15,
|
|
|
|
|
})
|
2026-04-29 11:42:37 +02:00
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].entry.path).toBe('/vault/a.md')
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# External edit')
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(3)
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenNthCalledWith(2, 'validate_note_content', {
|
|
|
|
|
path: '/vault/a.md',
|
|
|
|
|
content: '# Original content',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-22 20:18:26 +02:00
|
|
|
it('falls back instead of reopening cached content when the note file disappeared', async () => {
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockResolvedValueOnce('# Other note')
|
|
|
|
|
.mockRejectedValueOnce(new Error('File does not exist: /vault/note/missing-cached.md'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
cacheNoteContent('/vault/note/missing-cached.md', '# Cached but stale')
|
|
|
|
|
const onMissingNotePath = vi.fn()
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ onMissingNotePath }))
|
|
|
|
|
await selectNote(result, { path: '/vault/other.md', title: 'Other' })
|
|
|
|
|
await selectNote(result, { path: '/vault/note/missing-cached.md', title: 'Missing cached' })
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs).toEqual([])
|
|
|
|
|
expect(result.current.activeTabPath).toBeNull()
|
|
|
|
|
expect(onMissingNotePath).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ path: '/vault/note/missing-cached.md', title: 'Missing cached' }),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
2026-04-18 17:30:49 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('deduplicates a late prefetch after note opening already started', async () => {
|
|
|
|
|
let resolveContent!: (value: string) => void
|
|
|
|
|
vi.mocked(mockInvoke).mockImplementationOnce(
|
|
|
|
|
() => new Promise<string>((resolve) => { resolveContent = resolve }),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
void result.current.handleSelectNote(makeEntry({ path: '/vault/note/rapid.md', title: 'Rapid' }))
|
|
|
|
|
prefetchNoteContent('/vault/note/rapid.md')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
resolveContent('# Rapid content')
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.tabs[0].content).toBe('# Rapid content')
|
|
|
|
|
})
|
2026-03-09 13:07:31 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('rapid switching safety', () => {
|
|
|
|
|
it('only activates the last note when switching rapidly', async () => {
|
|
|
|
|
let resolveA: (v: string) => void
|
|
|
|
|
let resolveB: (v: string) => void
|
|
|
|
|
vi.mocked(mockInvoke)
|
|
|
|
|
.mockImplementationOnce(() => new Promise<string>((r) => { resolveA = r as (v: string) => void }))
|
|
|
|
|
.mockImplementationOnce(() => new Promise<string>((r) => { resolveB = r as (v: string) => void }))
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement())
|
|
|
|
|
|
|
|
|
|
let selectADone = false
|
2026-03-17 16:52:23 +01:00
|
|
|
await act(async () => {
|
2026-03-09 13:07:31 +01:00
|
|
|
result.current.handleSelectNote(makeEntry({ path: '/vault/a.md', title: 'A' })).then(() => { selectADone = true })
|
2026-03-17 16:52:23 +01:00
|
|
|
await Promise.resolve()
|
2026-03-09 13:07:31 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let selectBDone = false
|
2026-03-17 16:52:23 +01:00
|
|
|
await act(async () => {
|
2026-03-09 13:07:31 +01:00
|
|
|
result.current.handleSelectNote(makeEntry({ path: '/vault/b.md', title: 'B' })).then(() => { selectBDone = true })
|
2026-03-17 16:52:23 +01:00
|
|
|
await Promise.resolve()
|
2026-03-09 13:07:31 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await act(async () => { resolveB!('# B content') })
|
|
|
|
|
await act(async () => { resolveA!('# A content') })
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => expect(selectADone && selectBDone).toBe(true))
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/b.md')
|
|
|
|
|
})
|
2026-04-18 09:19:29 +02:00
|
|
|
|
|
|
|
|
it('waits for beforeNavigate before switching away from the current note', async () => {
|
|
|
|
|
const beforeNavigate = vi.fn(() => createDeferred<void>().promise)
|
|
|
|
|
const deferred = createDeferred<void>()
|
|
|
|
|
beforeNavigate.mockReturnValueOnce(deferred.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ beforeNavigate }))
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
|
|
|
|
|
let replaceDone = false
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/b.md', title: 'B' }))
|
|
|
|
|
.then(() => { replaceDone = true })
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(beforeNavigate).toHaveBeenCalledWith('/vault/a.md', '/vault/b.md')
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/a.md')
|
|
|
|
|
expect(replaceDone).toBe(false)
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
deferred.resolve(undefined)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => expect(replaceDone).toBe(true))
|
|
|
|
|
expectSingleActiveTab(result, '/vault/b.md')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-29 23:26:31 +02:00
|
|
|
it('records the requested note while the current note is still saving', async () => {
|
|
|
|
|
const deferred = createDeferred<void>()
|
|
|
|
|
const beforeNavigate = vi.fn().mockReturnValueOnce(deferred.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ beforeNavigate }))
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
void result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/b.md', title: 'B' }))
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/a.md')
|
|
|
|
|
expect(result.current.requestedActiveTabPathRef.current).toBe('/vault/b.md')
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
deferred.resolve(undefined)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => expect(result.current.activeTabPath).toBe('/vault/b.md'))
|
|
|
|
|
expect(result.current.requestedActiveTabPathRef.current).toBe('/vault/b.md')
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-18 09:19:29 +02:00
|
|
|
it('keeps only the latest target when note switches overlap during beforeNavigate', async () => {
|
|
|
|
|
const first = createDeferred<void>()
|
|
|
|
|
const second = createDeferred<void>()
|
|
|
|
|
const beforeNavigate = vi.fn()
|
|
|
|
|
.mockReturnValueOnce(first.promise)
|
|
|
|
|
.mockReturnValueOnce(second.promise)
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ beforeNavigate }))
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
|
|
|
|
|
let switchToBDone = false
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/b.md', title: 'B' }))
|
|
|
|
|
.then(() => { switchToBDone = true })
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let switchToCDone = false
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/c.md', title: 'C' }))
|
|
|
|
|
.then(() => { switchToCDone = true })
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
first.resolve(undefined)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/a.md')
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
second.resolve(undefined)
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => expect(switchToBDone && switchToCDone).toBe(true))
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/c.md')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the current note active when beforeNavigate fails', async () => {
|
|
|
|
|
const beforeNavigate = vi.fn().mockRejectedValueOnce(new Error('save failed'))
|
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useTabManagement({ beforeNavigate }))
|
|
|
|
|
await selectNote(result, { path: '/vault/a.md', title: 'A' })
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/b.md', title: 'B' }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.current.activeTabPath).toBe('/vault/a.md')
|
2026-04-29 23:26:31 +02:00
|
|
|
expect(result.current.requestedActiveTabPathRef.current).toBe('/vault/a.md')
|
2026-04-18 09:19:29 +02:00
|
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
|
|
|
'Failed to persist note before navigation:',
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
)
|
|
|
|
|
warnSpy.mockRestore()
|
|
|
|
|
})
|
2026-03-09 13:07:31 +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
|
|
|
})
|