Files
tolaria/src/components/NoteList.keyboard.test.tsx

178 lines
5.4 KiB
TypeScript
Raw Normal View History

import { useState } from 'react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NoteList } from './NoteList'
import {
allSelection,
makeIndexedEntry,
mockEntries,
} from '../test-utils/noteListTestUtils'
2026-04-19 03:37:33 +02:00
import type { SidebarSelection, VaultEntry } from '../types'
import * as tabManagement from '../hooks/useTabManagement'
function NoteListKeyboardHarness({
entries = mockEntries,
initialSelectedNote = null,
onOpen,
2026-04-19 03:37:33 +02:00
onEnterNeighborhood = () => {},
selectedNoteOverride,
2026-04-19 03:37:33 +02:00
selection = allSelection,
}: {
entries?: VaultEntry[]
initialSelectedNote?: VaultEntry | null
onOpen: (entry: VaultEntry) => void
2026-04-19 03:37:33 +02:00
onEnterNeighborhood?: (entry: VaultEntry) => void
selectedNoteOverride?: VaultEntry
2026-04-19 03:37:33 +02:00
selection?: SidebarSelection
}) {
const [selectedNote, setSelectedNote] = useState<VaultEntry | null>(initialSelectedNote)
const visibleSelectedNote = selectedNoteOverride ?? selectedNote
const handleOpen = (entry: VaultEntry) => {
setSelectedNote(entry)
onOpen(entry)
}
return (
<NoteList
entries={entries}
2026-04-19 03:37:33 +02:00
selection={selection}
selectedNote={visibleSelectedNote}
noteListFilter="open"
onNoteListFilterChange={() => {}}
onSelectNote={handleOpen}
onReplaceActiveTab={handleOpen}
2026-04-19 03:37:33 +02:00
onEnterNeighborhood={onEnterNeighborhood}
onCreateNote={() => {}}
/>
)
}
describe('NoteList keyboard activation', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('focuses the list on click and continues arrow navigation from the clicked note', async () => {
const onOpen = vi.fn()
render(<NoteListKeyboardHarness onOpen={onOpen} />)
fireEvent.click(screen.getByText('Facebook Ads Strategy'))
const container = screen.getByTestId('note-list-container')
await waitFor(() => {
expect(document.activeElement).toBe(container)
expect(
container.querySelector('[data-highlighted="true"]')?.getAttribute('data-note-path'),
).toBe(mockEntries[1].path)
})
fireEvent.keyDown(container, { key: 'ArrowDown' })
await waitFor(() => {
expect(onOpen).toHaveBeenNthCalledWith(1, mockEntries[1])
expect(onOpen).toHaveBeenNthCalledWith(2, mockEntries[2])
})
})
it('navigates from global arrow keys when the editor is not focused', async () => {
const onOpen = vi.fn()
render(
<>
<button type="button">Outside</button>
<NoteListKeyboardHarness onOpen={onOpen} />
</>,
)
screen.getByText('Outside').focus()
fireEvent.keyDown(window, { key: 'ArrowDown' })
await waitFor(() => {
expect(onOpen).toHaveBeenCalledWith(mockEntries[0])
})
})
2026-04-19 03:37:33 +02:00
it('supports Cmd+Enter to pivot the highlighted note into Neighborhood mode', async () => {
const onOpen = vi.fn()
const onEnterNeighborhood = vi.fn()
render(
<NoteListKeyboardHarness
onOpen={onOpen}
onEnterNeighborhood={onEnterNeighborhood}
selection={{ kind: 'entity', entry: mockEntries[0] }}
/>,
)
const container = screen.getByTestId('note-list-container')
fireEvent.keyDown(container, { key: 'ArrowDown' })
fireEvent.keyDown(container, { key: 'ArrowDown' })
fireEvent.keyDown(container, { key: 'Enter', metaKey: true })
await waitFor(() => {
expect(onOpen).toHaveBeenLastCalledWith(mockEntries[4])
expect(onEnterNeighborhood).toHaveBeenCalledWith(mockEntries[4])
})
})
it('prefetches note content on hover so click opens can use the warm path', () => {
const prefetchSpy = vi.spyOn(tabManagement, 'prefetchNoteContent').mockImplementation(() => {})
render(<NoteListKeyboardHarness onOpen={vi.fn()} />)
const noteRow = screen.getByText('Facebook Ads Strategy').closest('[data-note-path]')
expect(noteRow).not.toBeNull()
fireEvent.mouseEnter(noteRow!)
expect(prefetchSpy).toHaveBeenCalledWith(mockEntries[1])
})
it('keeps repeated large-note navigation broad for raw prefetch but narrow for parsed warmup', async () => {
vi.useFakeTimers()
try {
const largeEntries = Array.from({ length: 20 }, (_, index) => makeIndexedEntry(index, {
fileSize: 40 * 1024,
}))
const onOpen = vi.fn()
const prefetchSpy = vi.spyOn(tabManagement, 'prefetchNoteContent').mockImplementation(() => {})
const { rerender } = render(
<NoteListKeyboardHarness
entries={largeEntries}
onOpen={onOpen}
selectedNoteOverride={largeEntries[8]}
/>,
)
await act(async () => {
await vi.advanceTimersByTimeAsync(1_500)
})
expect(prefetchSpy).toHaveBeenCalledTimes(6)
expect(prefetchSpy.mock.calls[0][1]).toEqual({ parsedBlockPreload: true })
for (const call of prefetchSpy.mock.calls.slice(1)) {
expect(call[1]).toEqual({ parsedBlockPreload: false })
}
prefetchSpy.mockClear()
rerender(
<NoteListKeyboardHarness
entries={largeEntries}
onOpen={onOpen}
selectedNoteOverride={largeEntries[9]}
/>,
)
await act(async () => {
await vi.advanceTimersByTimeAsync(1_500)
})
expect(prefetchSpy).toHaveBeenCalledTimes(6)
expect(prefetchSpy.mock.calls[0][1]).toEqual({ parsedBlockPreload: true })
for (const call of prefetchSpy.mock.calls.slice(1)) {
expect(call[1]).toEqual({ parsedBlockPreload: false })
}
} finally {
vi.useRealTimers()
}
})
})