fix: debounce untitled H1 filename sync

This commit is contained in:
lucaronin
2026-04-10 17:18:48 +02:00
parent 1e36cd2855
commit c8db92c92d
2 changed files with 221 additions and 29 deletions

View File

@@ -1,14 +1,17 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useAppSave } from './useAppSave'
import type { VaultEntry } from '../types'
import { isTauri } from '../mock-tauri'
import { invoke } from '@tauri-apps/api/core'
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
isTauri: vi.fn(() => false),
mockInvoke: vi.fn().mockResolvedValue(undefined),
updateMockContent: vi.fn(),
}))
function makeEntry(path: string, title = 'Test', filename = 'test.md'): VaultEntry {
@@ -32,12 +35,18 @@ describe('useAppSave', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
vi.mocked(isTauri).mockReturnValue(false)
deps.unsavedPaths = new Set()
deps.tabs = []
deps.activeTabPath = null
deps.handleRenameNote.mockResolvedValue(undefined)
})
afterEach(() => {
vi.useRealTimers()
})
function renderSave(overrides = {}) {
return renderHook(() => useAppSave({ ...deps, ...overrides }))
}
@@ -91,4 +100,85 @@ describe('useAppSave', () => {
const { result } = renderSave()
expect(typeof result.current.handleContentChange).toBe('function')
})
it('debounces untitled H1 auto-rename until the user pauses typing', async () => {
vi.useFakeTimers()
vi.mocked(isTauri).mockReturnValue(true)
vi.mocked(invoke).mockImplementation(async (command: string, args?: Record<string, unknown>) => {
if (command === 'save_note_content') return undefined
if (command === 'auto_rename_untitled') return { new_path: '/vault/fresh-title.md', updated_files: 0 }
if (command === 'reload_vault_entry') return makeEntry('/vault/fresh-title.md', 'Fresh Title', 'fresh-title.md')
if (command === 'get_note_content' && args?.path === '/vault/fresh-title.md') return '# Fresh Title\n\nBody'
return undefined
})
const entry = makeEntry('/vault/untitled-note-123.md', 'Untitled Note 123', 'untitled-note-123.md')
const tabs = [{ entry, content: '# Fresh Title\n\nBody' }]
const { result } = renderSave({
tabs,
activeTabPath: entry.path,
unsavedPaths: new Set([entry.path]),
})
await act(async () => {
result.current.handleContentChange(entry.path, '# Fresh Title\n\nBody')
await vi.advanceTimersByTimeAsync(500)
})
expect(vi.mocked(invoke)).not.toHaveBeenCalledWith('auto_rename_untitled', expect.anything())
await act(async () => {
await vi.advanceTimersByTimeAsync(2_499)
})
expect(vi.mocked(invoke)).not.toHaveBeenCalledWith('auto_rename_untitled', expect.anything())
await act(async () => {
await vi.advanceTimersByTimeAsync(1)
})
expect(vi.mocked(invoke)).toHaveBeenCalledWith('auto_rename_untitled', {
vaultPath: '/vault',
notePath: entry.path,
})
expect(deps.replaceEntry).toHaveBeenCalledWith(
entry.path,
expect.objectContaining({ path: '/vault/fresh-title.md', filename: 'fresh-title.md' }),
'# Fresh Title\n\nBody',
)
})
it('cancels a pending untitled auto-rename when the user navigates away', async () => {
vi.useFakeTimers()
vi.mocked(isTauri).mockReturnValue(true)
vi.mocked(invoke).mockImplementation(async (command: string) => {
if (command === 'save_note_content') return undefined
if (command === 'auto_rename_untitled') return { new_path: '/vault/fresh-title.md', updated_files: 0 }
return undefined
})
const entry = makeEntry('/vault/untitled-note-123.md', 'Untitled Note 123', 'untitled-note-123.md')
const tabs = [{ entry, content: '# Fresh Title\n\nBody' }]
const { result, rerender } = renderHook(
({ currentActiveTabPath }: { currentActiveTabPath: string | null }) => useAppSave({
...deps,
tabs,
activeTabPath: currentActiveTabPath,
unsavedPaths: new Set([entry.path]),
}),
{ initialProps: { currentActiveTabPath: entry.path } },
)
await act(async () => {
result.current.handleContentChange(entry.path, '# Fresh Title\n\nBody')
await vi.advanceTimersByTimeAsync(500)
})
rerender({ currentActiveTabPath: '/vault/other.md' })
await act(async () => {
await vi.advanceTimersByTimeAsync(2_500)
})
expect(vi.mocked(invoke)).not.toHaveBeenCalledWith('auto_rename_untitled', expect.anything())
})
})