refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
import { render, screen } from '@testing-library/react'
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
|
import { NoteList } from './NoteList'
|
2026-04-12 01:35:34 +02:00
|
|
|
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS } from '../constants/appStorage'
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
import type { VaultEntry, SidebarSelection } from '../types'
|
|
|
|
|
|
|
|
|
|
const localStorageMock = (() => {
|
|
|
|
|
let store: Record<string, string> = {}
|
|
|
|
|
return {
|
|
|
|
|
getItem: vi.fn((key: string) => store[key] ?? null),
|
|
|
|
|
setItem: vi.fn((key: string, value: string) => { store[key] = value }),
|
|
|
|
|
removeItem: vi.fn((key: string) => { delete store[key] }),
|
|
|
|
|
clear: vi.fn(() => { store = {} }),
|
|
|
|
|
}
|
|
|
|
|
})()
|
|
|
|
|
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
|
|
|
|
|
|
|
|
|
|
function makeEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {
|
|
|
|
|
return {
|
|
|
|
|
path: '/test/note.md', filename: 'note.md', title: 'Test Note',
|
|
|
|
|
isA: 'Note', aliases: [], belongsTo: [], relatedTo: [],
|
|
|
|
|
status: null, owner: null, cadence: null, archived: false,
|
2026-04-06 12:21:56 +02:00
|
|
|
modifiedAt: 1700000000,
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
createdAt: null, fileSize: 100, snippet: '', wordCount: 0,
|
|
|
|
|
relationships: {}, icon: null, color: null, order: null,
|
|
|
|
|
sidebarLabel: null, template: null, sort: null,
|
|
|
|
|
outgoingLinks: [], properties: {},
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const noop = vi.fn()
|
|
|
|
|
|
|
|
|
|
function renderNoteList(props: {
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
selection: SidebarSelection
|
|
|
|
|
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
|
|
|
|
updateEntry?: (path: string, patch: Partial<VaultEntry>) => void
|
|
|
|
|
}) {
|
|
|
|
|
return render(
|
|
|
|
|
<NoteList
|
|
|
|
|
entries={props.entries}
|
|
|
|
|
selection={props.selection}
|
|
|
|
|
selectedNote={null}
|
|
|
|
|
onSelectNote={noop}
|
|
|
|
|
onReplaceActiveTab={noop}
|
|
|
|
|
onCreateNote={noop}
|
|
|
|
|
onUpdateTypeSort={props.onUpdateTypeSort}
|
|
|
|
|
updateEntry={props.updateEntry}
|
|
|
|
|
/>,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => { localStorageMock.clear() })
|
|
|
|
|
|
|
|
|
|
describe('useNoteListSort (via NoteList)', () => {
|
|
|
|
|
it('renders notes sorted by modified date by default', () => {
|
|
|
|
|
const entries = [
|
|
|
|
|
makeEntry({ path: '/a.md', title: 'Alpha', modifiedAt: 1000 }),
|
|
|
|
|
makeEntry({ path: '/b.md', title: 'Beta', modifiedAt: 3000 }),
|
|
|
|
|
makeEntry({ path: '/c.md', title: 'Charlie', modifiedAt: 2000 }),
|
|
|
|
|
]
|
|
|
|
|
renderNoteList({ entries, selection: { kind: 'filter', filter: 'all' } })
|
|
|
|
|
const items = screen.getAllByText(/Alpha|Beta|Charlie/)
|
|
|
|
|
expect(items[0].textContent).toBe('Beta')
|
|
|
|
|
expect(items[1].textContent).toBe('Charlie')
|
|
|
|
|
expect(items[2].textContent).toBe('Alpha')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('reads sort from type document for sectionGroup selection', () => {
|
2026-03-16 16:17:58 +01:00
|
|
|
const typeDoc = makeEntry({ path: '/note.md', title: 'Note', isA: 'Type', sort: 'title:asc' })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const entries = [
|
|
|
|
|
typeDoc,
|
|
|
|
|
makeEntry({ path: '/c.md', title: 'Charlie', modifiedAt: 3000 }),
|
|
|
|
|
makeEntry({ path: '/a.md', title: 'Alpha', modifiedAt: 1000 }),
|
|
|
|
|
makeEntry({ path: '/b.md', title: 'Beta', modifiedAt: 2000 }),
|
|
|
|
|
]
|
|
|
|
|
renderNoteList({ entries, selection: { kind: 'sectionGroup', type: 'Note', label: 'Notes' } })
|
|
|
|
|
const items = screen.getAllByText(/Alpha|Beta|Charlie/)
|
|
|
|
|
expect(items[0].textContent).toBe('Alpha')
|
|
|
|
|
expect(items[1].textContent).toBe('Beta')
|
|
|
|
|
expect(items[2].textContent).toBe('Charlie')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows type title as header for sectionGroup selection', () => {
|
2026-03-16 16:17:58 +01:00
|
|
|
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type' })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
renderNoteList({ entries: [typeDoc], selection: { kind: 'sectionGroup', type: 'Project', label: 'Projects' } })
|
|
|
|
|
expect(screen.getByText('Project')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('migrates localStorage sort to type frontmatter when type has no sort', () => {
|
2026-04-12 01:35:34 +02:00
|
|
|
localStorageMock.setItem(APP_STORAGE_KEYS.sortPreferences, JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const onUpdateTypeSort = vi.fn()
|
|
|
|
|
const updateEntry = vi.fn()
|
2026-03-16 16:17:58 +01:00
|
|
|
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: null })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const entries = [typeDoc, makeEntry()]
|
|
|
|
|
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries,
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Project', label: 'Projects' },
|
|
|
|
|
onUpdateTypeSort,
|
|
|
|
|
updateEntry,
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-16 16:17:58 +01:00
|
|
|
expect(onUpdateTypeSort).toHaveBeenCalledWith('/project.md', 'sort', 'title:asc')
|
|
|
|
|
expect(updateEntry).toHaveBeenCalledWith('/project.md', { sort: 'title:asc' })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not migrate if type already has sort', () => {
|
2026-04-12 01:35:34 +02:00
|
|
|
localStorageMock.setItem(APP_STORAGE_KEYS.sortPreferences, JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const onUpdateTypeSort = vi.fn()
|
|
|
|
|
const updateEntry = vi.fn()
|
2026-03-16 16:17:58 +01:00
|
|
|
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: 'modified:desc' })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const entries = [typeDoc, makeEntry()]
|
|
|
|
|
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries,
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Project', label: 'Projects' },
|
|
|
|
|
onUpdateTypeSort,
|
|
|
|
|
updateEntry,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(onUpdateTypeSort).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('falls back to modified when property sort references missing property', () => {
|
2026-04-12 01:35:34 +02:00
|
|
|
localStorageMock.setItem(APP_STORAGE_KEYS.sortPreferences, JSON.stringify({ '__list__': { option: 'property:priority', direction: 'asc' } }))
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const entries = [
|
|
|
|
|
makeEntry({ path: '/a.md', title: 'Alpha', modifiedAt: 1000, properties: {} }),
|
|
|
|
|
makeEntry({ path: '/b.md', title: 'Beta', modifiedAt: 3000, properties: {} }),
|
|
|
|
|
]
|
|
|
|
|
renderNoteList({ entries, selection: { kind: 'filter', filter: 'all' } })
|
|
|
|
|
const items = screen.getAllByText(/Alpha|Beta/)
|
|
|
|
|
// Should be sorted by modified desc (fallback), so Beta first
|
|
|
|
|
expect(items[0].textContent).toBe('Beta')
|
|
|
|
|
expect(items[1].textContent).toBe('Alpha')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses property sort when property exists in entries', () => {
|
2026-04-12 01:35:34 +02:00
|
|
|
localStorageMock.setItem(APP_STORAGE_KEYS.sortPreferences, JSON.stringify({ '__list__': { option: 'property:priority', direction: 'asc' } }))
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const entries = [
|
|
|
|
|
makeEntry({ path: '/b.md', title: 'Beta', modifiedAt: 3000, properties: { priority: 2 } }),
|
|
|
|
|
makeEntry({ path: '/a.md', title: 'Alpha', modifiedAt: 1000, properties: { priority: 1 } }),
|
|
|
|
|
]
|
|
|
|
|
renderNoteList({ entries, selection: { kind: 'filter', filter: 'all' } })
|
|
|
|
|
const items = screen.getAllByText(/Alpha|Beta/)
|
|
|
|
|
// Should be sorted by priority asc: Alpha (1) first, then Beta (2)
|
|
|
|
|
expect(items[0].textContent).toBe('Alpha')
|
|
|
|
|
expect(items[1].textContent).toBe('Beta')
|
|
|
|
|
})
|
2026-04-12 01:35:34 +02:00
|
|
|
|
|
|
|
|
it('reads legacy list sort preferences when Tolaria key is absent', () => {
|
|
|
|
|
localStorageMock.setItem(LEGACY_APP_STORAGE_KEYS.sortPreferences, JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
|
|
|
|
|
const entries = [
|
|
|
|
|
makeEntry({ path: '/c.md', title: 'Charlie', modifiedAt: 3000 }),
|
|
|
|
|
makeEntry({ path: '/a.md', title: 'Alpha', modifiedAt: 1000 }),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
renderNoteList({ entries, selection: { kind: 'filter', filter: 'all' } })
|
|
|
|
|
const items = screen.getAllByText(/Alpha|Charlie/)
|
|
|
|
|
expect(items[0].textContent).toBe('Alpha')
|
|
|
|
|
expect(items[1].textContent).toBe('Charlie')
|
|
|
|
|
})
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
})
|