Files
tolaria/src/components/QuickOpenPalette.test.tsx
Luca Rossi 89d0e39ad2 feat: note templates per type (💡 Note templates per tipo) (#170)
* feat: add template field to type entries and template-aware note creation

- Rust: add template field to Frontmatter, VaultEntry, SKIP_KEYS
- Rust: support YAML block scalar (|) for multi-line strings in frontmatter
- Rust: fix value continuation detection to handle block scalars properly
- TypeScript: add template to VaultEntry, buildNewEntry, frontmatterToEntryPatch
- Add DEFAULT_TEMPLATES for Project, Person, Responsibility, Experiment
- resolveNewNote/buildNoteContent accept optional template parameter
- handleCreateNote and handleCreateNoteImmediate look up type template
- Tests for all new behavior (Rust + TypeScript)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: wire template UI through Sidebar and App, add TypeCustomizePopover template section

- Add template textarea with debounced save to TypeCustomizePopover
- Wire handleUpdateTypeTemplate through useEntryActions → Sidebar → App
- Add useDebouncedCallback hook for 500ms template save debounce
- Add tests for handleUpdateTypeTemplate and TypeCustomizePopover template UI
- Create design/note-templates.pen placeholder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: useRef type arg + template field in bulk mock entries

* style: rustfmt

---------

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>
2026-03-02 08:37:06 +01:00

153 lines
5.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { QuickOpenPalette } from './QuickOpenPalette'
import type { VaultEntry } from '../types'
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
path: '/vault/note/test.md',
filename: 'test.md',
title: 'Test Note',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: 'Active',
owner: null,
cadence: null,
archived: false,
trashed: false,
trashedAt: null,
modifiedAt: 1700000000,
createdAt: 1700000000,
fileSize: 100,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
template: null,
outgoingLinks: [],
...overrides,
})
const entries: VaultEntry[] = [
makeEntry({ path: '/vault/note/alpha.md', title: 'Alpha Project', isA: 'Project', modifiedAt: 1700000003 }),
makeEntry({ path: '/vault/note/beta.md', title: 'Beta Notes', isA: 'Note', modifiedAt: 1700000002 }),
makeEntry({ path: '/vault/note/gamma.md', title: 'Gamma Experiment', isA: 'Experiment', modifiedAt: 1700000001 }),
]
// jsdom doesn't implement scrollIntoView
Element.prototype.scrollIntoView = vi.fn()
describe('QuickOpenPalette', () => {
const onSelect = vi.fn()
const onClose = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('renders nothing when closed', () => {
const { container } = render(
<QuickOpenPalette open={false} entries={entries} onSelect={onSelect} onClose={onClose} />
)
expect(container.firstChild).toBeNull()
})
it('shows search input when open', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
expect(screen.getByPlaceholderText('Search notes...')).toBeInTheDocument()
})
it('shows entries sorted by most recently modified when no query', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
const items = screen.getAllByText(/Alpha|Beta|Gamma/)
expect(items[0].textContent).toBe('Alpha Project')
expect(items[1].textContent).toBe('Beta Notes')
})
it('filters entries by fuzzy search', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
const input = screen.getByPlaceholderText('Search notes...')
fireEvent.change(input, { target: { value: 'alpha' } })
expect(screen.getByText('Alpha Project')).toBeInTheDocument()
expect(screen.queryByText('Beta Notes')).not.toBeInTheDocument()
})
it('shows "No matching notes" when query has no results', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
const input = screen.getByPlaceholderText('Search notes...')
fireEvent.change(input, { target: { value: 'zzzzzzz' } })
expect(screen.getByText('No matching notes')).toBeInTheDocument()
})
it('shows type badge for entries with isA', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
expect(screen.getByText('Project')).toBeInTheDocument()
expect(screen.getByText('Experiment')).toBeInTheDocument()
})
it('calls onSelect and onClose when clicking an entry', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
fireEvent.click(screen.getByText('Beta Notes'))
expect(onSelect).toHaveBeenCalledWith(entries[1])
expect(onClose).toHaveBeenCalled()
})
it('calls onClose when pressing Escape', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
fireEvent.keyDown(window, { key: 'Escape' })
expect(onClose).toHaveBeenCalled()
})
it('navigates with arrow keys and selects with Enter', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
// Move down one
fireEvent.keyDown(window, { key: 'ArrowDown' })
// Select with Enter
fireEvent.keyDown(window, { key: 'Enter' })
// Should select the second entry (index 1)
expect(onSelect).toHaveBeenCalledWith(entries[1])
expect(onClose).toHaveBeenCalled()
})
it('does not go below the last item with ArrowDown', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
// Press down many times
for (let i = 0; i < 10; i++) {
fireEvent.keyDown(window, { key: 'ArrowDown' })
}
fireEvent.keyDown(window, { key: 'Enter' })
// Should select last entry
expect(onSelect).toHaveBeenCalledWith(entries[2])
})
it('does not go above first item with ArrowUp', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
fireEvent.keyDown(window, { key: 'ArrowUp' })
fireEvent.keyDown(window, { key: 'Enter' })
// Should still select first entry
expect(onSelect).toHaveBeenCalledWith(entries[0])
})
it('calls onClose when clicking the backdrop', () => {
render(<QuickOpenPalette open={true} entries={entries} onSelect={onSelect} onClose={onClose} />)
// Click the backdrop (outermost div)
const backdrop = screen.getByPlaceholderText('Search notes...').closest('.fixed')!
fireEvent.click(backdrop)
expect(onClose).toHaveBeenCalled()
})
})