test: add useEditorSaveWithLinks tests, remove dead useDropdownKeyboard (#191)

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Luca Rossi
2026-03-12 03:01:39 +01:00
committed by GitHub
parent 19e386ccda
commit f5db98417d
2 changed files with 135 additions and 48 deletions

View File

@@ -1,48 +0,0 @@
import { useCallback, useRef } from 'react'
function resolveEnterTarget(
highlightIndex: number, allFiltered: string[], showCreateOption: boolean, query: string,
): string | null {
if (highlightIndex >= 0 && highlightIndex < allFiltered.length) return allFiltered[highlightIndex]
if (showCreateOption && highlightIndex === allFiltered.length) return query.trim()
if (query.trim()) return query.trim()
return null
}
export function useDropdownKeyboard({
highlightIndex, setHighlightIndex, totalOptions, allFiltered, showCreateOption, query, onSave, onCancel,
}: {
highlightIndex: number; setHighlightIndex: (i: number) => void; totalOptions: number
allFiltered: string[]; showCreateOption: boolean; query: string
onSave: (v: string) => void; onCancel: () => void
}) {
const listRef = useRef<HTMLDivElement>(null)
const scrollHighlightedIntoView = useCallback((index: number) => {
const items = listRef.current?.querySelectorAll('[data-testid^="status-option-"], [data-testid="status-create-option"]')
items?.[index]?.scrollIntoView({ block: 'nearest' })
}, [])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
const next = e.key === 'ArrowDown'
? (highlightIndex < totalOptions - 1 ? highlightIndex + 1 : 0)
: (highlightIndex > 0 ? highlightIndex - 1 : totalOptions - 1)
setHighlightIndex(next)
scrollHighlightedIntoView(next)
} else if (e.key === 'Enter') {
e.preventDefault()
const target = resolveEnterTarget(highlightIndex, allFiltered, showCreateOption, query)
if (target) onSave(target)
} else if (e.key === 'Escape') {
e.preventDefault()
onCancel()
}
},
[highlightIndex, totalOptions, allFiltered, showCreateOption, query, onSave, onCancel, setHighlightIndex, scrollHighlightedIntoView],
)
return { listRef, handleKeyDown }
}

View File

@@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useEditorSaveWithLinks } from './useEditorSaveWithLinks'
const mockHandleContentChange = vi.fn()
const mockHandleSave = vi.fn()
const mockSavePendingForPath = vi.fn()
vi.mock('./useEditorSave', () => ({
useEditorSave: vi.fn(() => ({
handleContentChange: mockHandleContentChange,
handleSave: mockHandleSave,
savePendingForPath: mockSavePendingForPath,
})),
}))
describe('useEditorSaveWithLinks', () => {
let updateEntry: Mock
let setTabs: Mock
let setToastMessage: Mock
let onAfterSave: Mock
beforeEach(() => {
updateEntry = vi.fn()
setTabs = vi.fn()
setToastMessage = vi.fn()
onAfterSave = vi.fn()
mockHandleContentChange.mockClear()
mockHandleSave.mockClear()
mockSavePendingForPath.mockClear()
})
function renderHookWithLinks() {
return renderHook(() =>
useEditorSaveWithLinks({
updateEntry,
setTabs,
setToastMessage,
onAfterSave,
}),
)
}
it('handleContentChange delegates to useEditorSave handleContentChange', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/note.md', 'no links here')
})
expect(mockHandleContentChange).toHaveBeenCalledWith('/note.md', 'no links here')
})
it('handleContentChange calls updateEntry with extracted outgoing links when links change', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/note.md', 'see [[PageA]] and [[PageB]]')
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
outgoingLinks: ['PageA', 'PageB'],
})
})
it('handleContentChange does NOT call updateEntry again when links have not changed', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/note.md', 'text [[Alpha]] more text')
})
expect(updateEntry).toHaveBeenCalledTimes(1)
// Same link, different surrounding text
act(() => {
result.current.handleContentChange('/note.md', 'different text [[Alpha]] still')
})
// updateEntry should NOT have been called again — links unchanged
expect(updateEntry).toHaveBeenCalledTimes(1)
})
it('handleContentChange calls updateEntry again when links change on subsequent edit', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/note.md', 'see [[Alpha]]')
})
expect(updateEntry).toHaveBeenCalledTimes(1)
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
outgoingLinks: ['Alpha'],
})
// Now links change
act(() => {
result.current.handleContentChange('/note.md', 'see [[Alpha]] and [[Beta]]')
})
expect(updateEntry).toHaveBeenCalledTimes(2)
expect(updateEntry).toHaveBeenLastCalledWith('/note.md', {
outgoingLinks: ['Alpha', 'Beta'],
})
})
it('handleContentChange calls updateEntry with empty links on first call with no links', () => {
const { result } = renderHookWithLinks()
// First call with no links — prevLinksKeyRef starts as '' and extracted key is also ''
// but since they're equal, updateEntry should NOT be called
act(() => {
result.current.handleContentChange('/note.md', 'plain text no links')
})
// The initial ref is '' and no-links key is also '' — no change
expect(updateEntry).not.toHaveBeenCalled()
})
it('handles pipe-separated wikilinks (display text syntax)', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/note.md', 'see [[Target|Display Text]]')
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
outgoingLinks: ['Target'],
})
})
it('spreads all properties from useEditorSave onto the return value', () => {
const { result } = renderHookWithLinks()
// handleSave and savePendingForPath should be passed through from the mock
expect(result.current.handleSave).toBeDefined()
expect(result.current.savePendingForPath).toBeDefined()
})
})