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:
@@ -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 }
|
||||
}
|
||||
135
src/hooks/useEditorSaveWithLinks.test.ts
Normal file
135
src/hooks/useEditorSaveWithLinks.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user