test+design: add virtual list tests and design wireframes

Add 7 new tests covering virtual list behavior with large datasets:
- 9000-entry rendering without crash
- Items rendered via Virtuoso mock
- Search filtering on large dataset
- Sorting correctness with large dataset
- Section group filtering with mixed types
- Selection highlighting in virtualized list
- Click handler on virtualized items

Add design/performance-note-list.pen with 4 frames:
1. Default state — 9000+ notes with scrollbar
2. Scrolled mid-list — items 4500+
3. Search filtering — active query narrowing results
4. Empty search result — no matching notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-23 21:13:58 +01:00
parent 9770763674
commit f6a4ab14de
2 changed files with 381 additions and 0 deletions

View File

@@ -748,3 +748,111 @@ describe('NoteList — trash view', () => {
expect(screen.getByText('Trash is empty')).toBeInTheDocument()
})
})
// --- Virtual list performance tests ---
describe('NoteList — virtual list with large datasets', () => {
const makeEntry = (i: number, overrides?: Partial<VaultEntry>): VaultEntry => ({
path: `/vault/note/note-${i}.md`,
filename: `note-${i}.md`,
title: `Note ${i}`,
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
owner: null,
cadence: null,
archived: false,
trashed: false,
trashedAt: null,
modifiedAt: 1700000000 - i * 60,
createdAt: null,
fileSize: 500,
snippet: `Content of note ${i}`,
relationships: {},
icon: null,
color: null,
order: null,
...overrides,
})
it('renders 9000 entries without crashing', () => {
const largeDataset = Array.from({ length: 9000 }, (_, i) => makeEntry(i))
const { container } = render(
<NoteList entries={largeDataset} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
// Virtuoso mock renders all items; the real component only renders visible ones
expect(container.querySelector('[data-testid="virtuoso-mock"]')).toBeInTheDocument()
})
it('renders items from a large dataset via Virtuoso', () => {
const largeDataset = Array.from({ length: 500 }, (_, i) => makeEntry(i))
render(
<NoteList entries={largeDataset} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
expect(screen.getByText('Note 0')).toBeInTheDocument()
expect(screen.getByText('Note 499')).toBeInTheDocument()
})
it('search filters large dataset correctly', () => {
const entries = [
makeEntry(0, { title: 'Alpha Strategy' }),
...Array.from({ length: 998 }, (_, i) => makeEntry(i + 1, { title: `Filler Note ${i + 1}` })),
makeEntry(999, { title: 'Beta Strategy' }),
]
render(
<NoteList entries={entries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
fireEvent.click(screen.getByTitle('Search notes'))
fireEvent.change(screen.getByPlaceholderText('Search notes...'), { target: { value: 'Strategy' } })
expect(screen.getByText('Alpha Strategy')).toBeInTheDocument()
expect(screen.getByText('Beta Strategy')).toBeInTheDocument()
expect(screen.queryByText('Filler Note 1')).not.toBeInTheDocument()
})
it('sorting works with large dataset', () => {
const entries = [
makeEntry(0, { title: 'Zebra', modifiedAt: 1000 }),
makeEntry(1, { title: 'Alpha', modifiedAt: 3000 }),
...Array.from({ length: 100 }, (_, i) => makeEntry(i + 2, { title: `Mid ${i}`, modifiedAt: 2000 - i })),
]
render(
<NoteList entries={entries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
// Default sort is modified desc — Alpha (3000) should come first
const firstTitle = screen.getAllByText(/^Alpha$|^Zebra$/)[0]
expect(firstTitle.textContent).toBe('Alpha')
})
it('section group filter works with large mixed-type dataset', () => {
const entries = [
...Array.from({ length: 100 }, (_, i) => makeEntry(i, { isA: 'Project', title: `Project ${i}` })),
...Array.from({ length: 200 }, (_, i) => makeEntry(100 + i, { isA: 'Note', title: `Note ${i}` })),
]
render(
<NoteList entries={entries} selection={{ kind: 'sectionGroup', type: 'Project' }} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
expect(screen.getByText('Project 0')).toBeInTheDocument()
expect(screen.queryByText('Note 0')).not.toBeInTheDocument()
})
it('selection highlighting works in virtualized list', () => {
const entries = Array.from({ length: 100 }, (_, i) => makeEntry(i))
const selected = entries[5]
render(
<NoteList entries={entries} selection={allSelection} selectedNote={selected} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
expect(screen.getByText('Note 5')).toBeInTheDocument()
})
it('click handler works on virtualized items', () => {
noopReplace.mockClear()
const entries = Array.from({ length: 100 }, (_, i) => makeEntry(i))
render(
<NoteList entries={entries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
)
fireEvent.click(screen.getByText('Note 50'))
expect(noopReplace).toHaveBeenCalledWith(entries[50])
})
})